From a135aaaaef43d5b05bd2f7dd870d5e61037749a3 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Tue, 10 Mar 2026 18:42:07 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20moduulij=C3=A4rjestelm=C3=A4=20+=20k?= =?UTF-8?q?=C3=A4ytt=C3=A4j=C3=A4roolit=20+=20suhteellinen=20aika?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moduulijärjestelmä: yrityskohtaiset tabit (customers, support, leads, archive, changelog, settings) valittavissa checkboxeina yrityksen asetuksissa - Käyttäjäroolit: superadmin (pääkäyttäjä), admin (yritysadmin), user (käyttäjä) - Superadmin: kaikki oikeudet kuten ennen - Yritysadmin: muokkaa oman yrityksen asetuksia, moduuleita, postilaatikoita - Käyttäjä: peruskäyttö ilman hallintaoikeuksia - Päivitetty-kenttä näyttää suhteellista aikaa (15min sitten, 2h sitten, 3pv sitten) - DB: enabled_modules sarake companies-tauluun, role ENUM laajennettu - Automaattinen migraatio: vanhat admin → superadmin Co-Authored-By: Claude Opus 4.6 --- api.php | 75 ++++++++++++++++++++++++++++++++++++------- db.php | 74 ++++++++++++++++++++++++++++--------------- index.html | 28 +++++++++++++++- script.js | 93 +++++++++++++++++++++++++++++++++++++++++++++++------- style.css | 5 +++ 5 files changed, 225 insertions(+), 50 deletions(-) diff --git a/api.php b/api.php index 4a472ce..5bf5795 100644 --- a/api.php +++ b/api.php @@ -38,13 +38,32 @@ function requireAuth() { function requireAdmin() { requireAuth(); - if (($_SESSION['role'] ?? '') !== 'admin') { + $role = $_SESSION['role'] ?? ''; + if ($role !== 'admin' && $role !== 'superadmin') { http_response_code(403); echo json_encode(['error' => 'Vain ylläpitäjä voi tehdä tämän']); exit; } } +function requireSuperAdmin() { + requireAuth(); + if (($_SESSION['role'] ?? '') !== 'superadmin') { + http_response_code(403); + echo json_encode(['error' => 'Vain pääkäyttäjä voi tehdä tämän']); + exit; + } +} + +function isSuperAdmin(): bool { + return ($_SESSION['role'] ?? '') === 'superadmin'; +} + +function isCompanyAdmin(): bool { + $role = $_SESSION['role'] ?? ''; + return $role === 'admin' || $role === 'superadmin'; +} + function currentUser(): string { return $_SESSION['username'] ?? 'tuntematon'; } @@ -884,7 +903,7 @@ switch ($action) { $domainCompany = dbGetCompanyByDomain($host); $domainCompanyId = $domainCompany ? $domainCompany['id'] : ''; // Jos domain kuuluu tietylle yritykselle, vain sen yrityksen käyttäjät + adminit pääsevät sisään - if ($domainCompanyId && $u['role'] !== 'admin' && !in_array($domainCompanyId, $userCompanies)) { + if ($domainCompanyId && $u['role'] !== 'superadmin' && !in_array($domainCompanyId, $userCompanies)) { dbRecordLoginAttempt($ip); http_response_code(403); echo json_encode(['error' => 'Sinulla ei ole oikeutta kirjautua tälle sivustolle.']); @@ -956,6 +975,15 @@ switch ($action) { // Brändäystiedot domain-pohjaisesti (sama kuin branding-endpoint) $host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]); $branding = dbGetBranding($host); + // Aktiivisen yrityksen enabled_modules + $activeCompanyId = $_SESSION['company_id'] ?? ''; + $enabledModules = []; + foreach ($allCompanies as $comp) { + if ($comp['id'] === $activeCompanyId) { + $enabledModules = $comp['enabled_modules'] ?? []; + break; + } + } echo json_encode([ 'authenticated' => true, 'user_id' => $_SESSION['user_id'], @@ -966,6 +994,7 @@ switch ($action) { 'company_id' => $_SESSION['company_id'] ?? '', 'signatures' => $userSignatures, 'branding' => $branding, + 'enabled_modules' => $enabledModules, ]); } else { echo json_encode(['authenticated' => false]); @@ -1035,7 +1064,7 @@ switch ($action) { // ---------- USERS ---------- case 'users': - requireAdmin(); + requireSuperAdmin(); $users = dbLoadUsers(); $safe = array_map(function($u) { unset($u['password_hash']); @@ -1045,14 +1074,15 @@ switch ($action) { break; case 'user_create': - requireAdmin(); + requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $username = trim($input['username'] ?? ''); $password = $input['password'] ?? ''; $nimi = trim($input['nimi'] ?? ''); $email = trim($input['email'] ?? ''); - $role = ($input['role'] ?? 'user') === 'admin' ? 'admin' : 'user'; + $validRoles = ['superadmin', 'admin', 'user']; + $role = in_array($input['role'] ?? '', $validRoles) ? $input['role'] : 'user'; if (empty($username) || empty($password)) { http_response_code(400); echo json_encode(['error' => 'Käyttäjätunnus ja salasana vaaditaan']); @@ -1099,7 +1129,7 @@ switch ($action) { break; case 'user_update': - requireAdmin(); + requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -1111,7 +1141,10 @@ switch ($action) { } if (isset($input['nimi'])) $u['nimi'] = trim($input['nimi']); if (isset($input['email'])) $u['email'] = trim($input['email']); - if (isset($input['role'])) $u['role'] = $input['role'] === 'admin' ? 'admin' : 'user'; + if (isset($input['role'])) { + $validRoles = ['superadmin', 'admin', 'user']; + $u['role'] = in_array($input['role'], $validRoles) ? $input['role'] : 'user'; + } if (isset($input['companies'])) { $allCompanies = dbLoadCompanies(); $validIds = array_column($allCompanies, 'id'); @@ -1146,7 +1179,7 @@ switch ($action) { break; case 'user_delete': - requireAdmin(); + requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -2297,7 +2330,15 @@ switch ($action) { case 'companies_all': requireAdmin(); - echo json_encode(dbLoadCompanies()); + $all = dbLoadCompanies(); + if (isSuperAdmin()) { + echo json_encode($all); + } else { + // Yritysadmin näkee vain omat yrityksensä + $userCompanyIds = $_SESSION['companies'] ?? []; + $filtered = array_values(array_filter($all, fn($c) => in_array($c['id'], $userCompanyIds))); + echo json_encode($filtered); + } break; case 'all_mailboxes': @@ -2322,7 +2363,7 @@ switch ($action) { break; case 'company_create': - requireAdmin(); + requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = preg_replace('/[^a-z0-9-]/', '', strtolower(trim($input['id'] ?? ''))); @@ -2374,6 +2415,15 @@ switch ($action) { if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; + // Yritysadmin saa muokata vain omia yrityksiään + if (!isSuperAdmin()) { + $userCompanyIds = $_SESSION['companies'] ?? []; + if (!in_array($id, $userCompanyIds)) { + http_response_code(403); + echo json_encode(['error' => 'Ei oikeuksia muokata tätä yritystä']); + break; + } + } $companies = dbLoadCompanies(); $found = false; foreach ($companies as $c) { @@ -2385,6 +2435,9 @@ switch ($action) { } if (isset($input['primary_color'])) $c['primary_color'] = trim($input['primary_color']); if (isset($input['subtitle'])) $c['subtitle'] = trim($input['subtitle']); + if (isset($input['enabled_modules']) && is_array($input['enabled_modules'])) { + $c['enabled_modules'] = array_values($input['enabled_modules']); + } dbSaveCompany($c); $found = true; echo json_encode($c); @@ -2398,7 +2451,7 @@ switch ($action) { break; case 'company_delete': - requireAdmin(); + requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; diff --git a/db.php b/db.php index 359e7f9..f394261 100644 --- a/db.php +++ b/db.php @@ -137,7 +137,7 @@ function initDatabase(): void { username VARCHAR(100) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, nimi VARCHAR(255) NOT NULL, - role ENUM('admin','user') DEFAULT 'user', + role ENUM('superadmin','admin','user') DEFAULT 'user', email VARCHAR(255) DEFAULT '', luotu DATETIME ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", @@ -375,10 +375,21 @@ function initDatabase(): void { "ALTER TABLE tickets ADD COLUMN cc TEXT DEFAULT '' AFTER mailbox_id", "ALTER TABLE tickets ADD COLUMN priority VARCHAR(20) DEFAULT 'normaali' AFTER cc", "ALTER TABLE customers ADD COLUMN priority_emails TEXT DEFAULT '' AFTER lisatiedot", + "ALTER TABLE companies ADD COLUMN enabled_modules TEXT DEFAULT '' AFTER cors_origins", + "ALTER TABLE users MODIFY COLUMN role ENUM('superadmin','admin','user') DEFAULT 'user'", ]; foreach ($alters as $sql) { - try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa */ } + try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } } + // Kertaluontoinen migraatio: päivitä vanhat admin-käyttäjät superadminiksi + // (vain jos yhtään superadminia ei vielä ole) + try { + $result = $db->query("SELECT COUNT(*) AS cnt FROM users WHERE role = 'superadmin'"); + $row = $result->fetch_assoc(); + if ((int)($row['cnt'] ?? 0) === 0) { + $db->query("UPDATE users SET role = 'superadmin' WHERE role = 'admin'"); + } + } catch (\Throwable $e) { /* ohitetaan */ } } // ==================== YRITYKSET ==================== @@ -389,6 +400,9 @@ function dbLoadCompanies(): array { foreach ($companies as &$c) { $c['domains'] = _dbFetchColumn("SELECT domain FROM company_domains WHERE company_id = ?", [$c['id']]); $c['aktiivinen'] = (bool)$c['aktiivinen']; + // enabled_modules: JSON-array tai tyhjä (= kaikki päällä) + $raw = $c['enabled_modules'] ?? ''; + $c['enabled_modules'] = $raw ? (json_decode($raw, true) ?: []) : []; } return $companies; } @@ -397,23 +411,27 @@ function dbSaveCompany(array $company): void { $db = getDb(); $db->begin_transaction(); try { + $enabledModules = $company['enabled_modules'] ?? []; + $enabledModulesJson = is_array($enabledModules) ? json_encode($enabledModules) : ($enabledModules ?: ''); _dbExecute(" - INSERT INTO companies (id, nimi, luotu, aktiivinen, primary_color, subtitle, logo_file, api_key, cors_origins) - VALUES (:id, :nimi, :luotu, :aktiivinen, :primary_color, :subtitle, :logo_file, :api_key, :cors_origins) + INSERT INTO companies (id, nimi, luotu, aktiivinen, primary_color, subtitle, logo_file, api_key, cors_origins, enabled_modules) + VALUES (:id, :nimi, :luotu, :aktiivinen, :primary_color, :subtitle, :logo_file, :api_key, :cors_origins, :enabled_modules) ON DUPLICATE KEY UPDATE nimi = VALUES(nimi), aktiivinen = VALUES(aktiivinen), primary_color = VALUES(primary_color), subtitle = VALUES(subtitle), - logo_file = VALUES(logo_file), api_key = VALUES(api_key), cors_origins = VALUES(cors_origins) + logo_file = VALUES(logo_file), api_key = VALUES(api_key), cors_origins = VALUES(cors_origins), + enabled_modules = VALUES(enabled_modules) ", [ - 'id' => $company['id'], - 'nimi' => $company['nimi'], - 'luotu' => $company['luotu'] ?? date('Y-m-d H:i:s'), - 'aktiivinen' => $company['aktiivinen'] ?? true, - 'primary_color' => $company['primary_color'] ?? '#0f3460', - 'subtitle' => $company['subtitle'] ?? '', - 'logo_file' => $company['logo_file'] ?? '', - 'api_key' => $company['api_key'] ?? '', - 'cors_origins' => $company['cors_origins'] ?? '', + 'id' => $company['id'], + 'nimi' => $company['nimi'], + 'luotu' => $company['luotu'] ?? date('Y-m-d H:i:s'), + 'aktiivinen' => $company['aktiivinen'] ?? true, + 'primary_color' => $company['primary_color'] ?? '#0f3460', + 'subtitle' => $company['subtitle'] ?? '', + 'logo_file' => $company['logo_file'] ?? '', + 'api_key' => $company['api_key'] ?? '', + 'cors_origins' => $company['cors_origins'] ?? '', + 'enabled_modules' => $enabledModulesJson, ]); // Päivitä domainit @@ -451,23 +469,27 @@ function dbGetBranding(string $host): array { $logoUrl = !empty($company['logo_file']) ? "api.php?action=company_logo&company_id=" . urlencode($company['id']) : ''; + $rawModules = $company['enabled_modules'] ?? ''; + $enabledModules = $rawModules ? (json_decode($rawModules, true) ?: []) : []; return [ - 'found' => true, - 'company_id' => $company['id'], - 'nimi' => $company['nimi'], - 'primary_color' => $company['primary_color'] ?? '#0f3460', - 'subtitle' => $company['subtitle'] ?? '', - 'logo_url' => $logoUrl, + 'found' => true, + 'company_id' => $company['id'], + 'nimi' => $company['nimi'], + 'primary_color' => $company['primary_color'] ?? '#0f3460', + 'subtitle' => $company['subtitle'] ?? '', + 'logo_url' => $logoUrl, + 'enabled_modules' => $enabledModules, ]; } return [ - 'found' => false, - 'company_id' => '', - 'nimi' => 'Noxus Intra', - 'primary_color' => '#0f3460', - 'subtitle' => 'Hallintapaneeli', - 'logo_url' => '', + 'found' => false, + 'company_id' => '', + 'nimi' => 'Noxus Intra', + 'primary_color' => '#0f3460', + 'subtitle' => 'Hallintapaneeli', + 'logo_url' => '', + 'enabled_modules' => [], ]; } diff --git a/index.html b/index.html index f39e20b..3841068 100644 --- a/index.html +++ b/index.html @@ -606,6 +606,31 @@ + +
+

Käytössä olevat moduulit

+

Valitse mitkä välilehdet ovat käytössä tässä yrityksessä.

+
+ + + + + + +
+
@@ -816,7 +841,8 @@
diff --git a/script.js b/script.js index f6f934b..0c494e9 100644 --- a/script.js +++ b/script.js @@ -137,6 +137,7 @@ async function checkAuth() { currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null; currentUserSignatures = data.signatures || {}; if (data.branding) applyBranding(data.branding); + applyModules(data.enabled_modules || []); showDashboard(); } } catch (e) { /* not logged in */ } @@ -179,10 +180,12 @@ async function showDashboard() { loginScreen.style.display = 'none'; dashboard.style.display = 'block'; document.getElementById('user-info').textContent = currentUser.nimi || currentUser.username; - // Näytä admin-toiminnot vain adminille - document.getElementById('btn-users').style.display = currentUser.role === 'admin' ? '' : 'none'; - document.getElementById('tab-settings').style.display = currentUser.role === 'admin' ? '' : 'none'; - document.getElementById('btn-companies').style.display = currentUser.role === 'admin' ? '' : 'none'; + const isSuperAdmin = currentUser.role === 'superadmin'; + const isAdmin = currentUser.role === 'admin' || isSuperAdmin; + // Näytä admin-toiminnot roolin mukaan + document.getElementById('btn-users').style.display = isSuperAdmin ? '' : 'none'; + document.getElementById('tab-settings').style.display = isAdmin ? '' : 'none'; + document.getElementById('btn-companies').style.display = isAdmin ? '' : 'none'; // Yritysvalitsin populateCompanySelector(); // Avaa oikea tabi URL-hashin perusteella (tai customers oletuks) @@ -212,6 +215,7 @@ async function switchCompany(companyId) { try { const auth = await apiCall('check_auth'); if (auth.branding) applyBranding(auth.branding); + applyModules(auth.enabled_modules || []); } catch (e2) {} // Lataa uudelleen aktiivinen tab const hash = window.location.hash.replace('#', '') || 'customers'; @@ -415,6 +419,25 @@ function setText(id, value) { const el = document.getElementById(id); if (el) el function formatPrice(val) { return parseFloat(val || 0).toFixed(2).replace('.', ',') + ' €'; } function esc(str) { if (!str) return ''; const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } +function timeAgo(dateStr) { + if (!dateStr) return ''; + const date = new Date(dateStr.replace(' ', 'T')); + const now = new Date(); + const diffMs = now - date; + if (diffMs < 0) return 'juuri nyt'; + const diffSec = Math.floor(diffMs / 1000); + if (diffSec < 60) return 'juuri nyt'; + const diffMin = Math.floor(diffSec / 60); + if (diffMin < 60) return diffMin + ' min sitten'; + const diffHours = Math.floor(diffMin / 60); + if (diffHours < 24) return diffHours + 'h sitten'; + const diffDays = Math.floor(diffHours / 24); + if (diffDays < 7) return diffDays + 'pv sitten'; + if (diffDays < 30) return Math.floor(diffDays / 7) + 'vk sitten'; + // Yli kuukausi → näytä päivämäärä + return dateStr.substring(0, 10); +} + // Search & Sort searchInput.addEventListener('input', () => renderTable()); document.querySelectorAll('th[data-sort]').forEach(th => { @@ -764,7 +787,7 @@ function showLeadDetail(id) {
Sähköposti
${detailLink(l.sahkoposti, 'email')}
Osoite
${detailVal([l.osoite, l.kaupunki].filter(Boolean).join(', '))}
Lisätty
${detailVal(l.luotu)} (${esc(l.luoja || '')})
- ${l.muokattu ? `
Muokattu
${esc(l.muokattu)} (${esc(l.muokkaaja || '')})
` : ''} + ${l.muokattu ? `
Muokattu
${timeAgo(l.muokattu)} (${esc(l.muokkaaja || '')})
` : ''}
${l.muistiinpanot ? `
MUISTIINPANOT
${esc(l.muistiinpanot)}
` : ''} `; @@ -943,7 +966,7 @@ async function loadUsers() { ${esc(u.username)} ${esc(u.nimi)} ${esc(u.email || '')} - ${u.role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'} + ${u.role === 'superadmin' ? 'Pääkäyttäjä' : (u.role === 'admin' ? 'Yritysadmin' : 'Käyttäjä')} ${esc(u.luotu)} @@ -1178,7 +1201,7 @@ function renderTickets() { ${esc(t.mailbox_name || t.from_name || t.from_email)} ${t.customer_name ? esc(t.customer_name) : '-'} ${lastType} ${t.message_count} - ${esc((t.updated || '').substring(0, 16))} + ${timeAgo(t.updated)} `; }).join(''); // Re-attach checkbox listeners @@ -1965,6 +1988,7 @@ async function loadCompaniesTab() { function renderCompaniesTable() { const tbody = document.getElementById('companies-tbody'); + const superAdmin = currentUser?.role === 'superadmin'; tbody.innerHTML = companiesTabData.map(c => ` ${esc(c.id)} ${esc(c.nimi)} @@ -1973,9 +1997,12 @@ function renderCompaniesTable() { ${c.aktiivinen !== false ? 'Aktiivinen' : 'Ei aktiivinen'} - + ${superAdmin ? `` : ''} `).join(''); + // Piilota "Lisää yritys" nappi jos ei superadmin + const addBtn = document.getElementById('btn-add-company'); + if (addBtn) addBtn.style.display = superAdmin ? '' : 'none'; document.getElementById('companies-list-view').style.display = ''; document.getElementById('company-detail-view').style.display = 'none'; } @@ -2035,6 +2062,14 @@ async function showCompanyDetail(id) { logoPreview.style.display = 'none'; } + // Moduuli-checkboxit + const enabledMods = comp?.enabled_modules || []; + document.querySelectorAll('#modules-checkboxes input[data-module]').forEach(cb => { + const mod = cb.dataset.module; + // Jos enabled_modules on tyhjä → kaikki päällä (oletus) + cb.checked = enabledMods.length === 0 ? DEFAULT_MODULES.includes(mod) : enabledMods.includes(mod); + }); + // Vaihda aktiivinen yritys jotta API-kutsut kohdistuvat oikein await apiCall('company_switch', 'POST', { company_id: id }); @@ -2160,21 +2195,27 @@ document.getElementById('btn-save-company-settings').addEventListener('click', a const primary_color = document.getElementById('company-edit-color').value; const domainsText = document.getElementById('company-edit-domains').value; const domains = domainsText.split('\n').map(d => d.trim()).filter(d => d); + // Moduulit + const enabled_modules = []; + document.querySelectorAll('#modules-checkboxes input[data-module]:checked').forEach(cb => { + enabled_modules.push(cb.dataset.module); + }); try { - await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi, subtitle, primary_color, domains }); + await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi, subtitle, primary_color, domains, enabled_modules }); alert('Asetukset tallennettu!'); // Päivitä paikalliset tiedot const comp = companiesTabData.find(c => c.id === currentCompanyDetail); - if (comp) { comp.nimi = nimi; comp.subtitle = subtitle; comp.primary_color = primary_color; comp.domains = domains; } + if (comp) { comp.nimi = nimi; comp.subtitle = subtitle; comp.primary_color = primary_color; comp.domains = domains; comp.enabled_modules = enabled_modules; } const avail = availableCompanies.find(c => c.id === currentCompanyDetail); if (avail) avail.nimi = nimi; populateCompanySelector(); - // Jos tämä on aktiivinen yritys → päivitä brändäys heti + // Jos tämä on aktiivinen yritys → päivitä brändäys ja moduulit heti if (currentCompany && currentCompany.id === currentCompanyDetail) { applyBranding({ nimi, subtitle, primary_color, logo_url: comp?.logo_file ? 'api.php?action=company_logo&company_id=' + encodeURIComponent(currentCompanyDetail) + '&t=' + Date.now() : '' }); + applyModules(enabled_modules); } } catch (e) { alert(e.message); } }); @@ -2275,7 +2316,7 @@ async function loadCompanyUsers(companyId) { return ``; }).join(''); } catch (e) { console.error(e); } @@ -2296,6 +2337,34 @@ async function toggleCompanyUser(userId, companyId, add) { } catch (e) { alert(e.message); } } +// ==================== MODUULIT ==================== + +const ALL_MODULES = ['customers', 'support', 'leads', 'archive', 'changelog', 'settings']; +const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings']; + +function applyModules(modules) { + // Jos tyhjä array → kaikki moduulit päällä (fallback) + const enabled = (modules && modules.length > 0) ? modules : ALL_MODULES; + const isAdminUser = currentUser?.role === 'admin' || currentUser?.role === 'superadmin'; + ALL_MODULES.forEach(mod => { + const tabBtn = document.querySelector(`.tab[data-tab="${mod}"]`); + if (tabBtn) { + // settings-tabi näkyy vain adminille/superadminille + if (mod === 'settings') { + tabBtn.style.display = (enabled.includes(mod) && isAdminUser) ? '' : 'none'; + } else { + tabBtn.style.display = enabled.includes(mod) ? '' : 'none'; + } + } + }); + // Jos aktiivinen tabi on piilotettu → vaihda ensimmäiseen näkyvään + const activeTab = document.querySelector('.tab.active'); + if (activeTab && activeTab.style.display === 'none') { + const firstVisible = document.querySelector('.tab[data-tab]:not([style*="display: none"])'); + if (firstVisible) switchToTab(firstVisible.dataset.tab); + } +} + // ==================== BRANDING ==================== function applyBranding(branding) { diff --git a/style.css b/style.css index b59ce7c..01a25da 100644 --- a/style.css +++ b/style.css @@ -1033,6 +1033,11 @@ span.empty { letter-spacing: 0.3px; } +.role-superadmin { + background: #7c3aed; + color: #fff; +} + .role-admin { background: var(--primary-color); color: #fff;