feat: moduulijärjestelmä + käyttäjäroolit + suhteellinen aika
- 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 <noreply@anthropic.com>
This commit is contained in:
75
api.php
75
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'] ?? '';
|
||||
|
||||
74
db.php
74
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' => [],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
28
index.html
28
index.html
@@ -606,6 +606,31 @@
|
||||
<textarea id="company-edit-domains" rows="3" placeholder="intra.yritys.fi intra.toinen.fi" style="font-family:monospace;font-size:0.85rem;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Moduulit -->
|
||||
<div style="margin-bottom:1.5rem;">
|
||||
<h4 style="color:#0f3460;margin-bottom:0.5rem;font-size:0.95rem;">Käytössä olevat moduulit</h4>
|
||||
<p style="color:#888;font-size:0.82rem;margin-bottom:0.75rem;">Valitse mitkä välilehdet ovat käytössä tässä yrityksessä.</p>
|
||||
<div id="modules-checkboxes" style="display:flex;flex-direction:column;gap:0.4rem;">
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||
<input type="checkbox" data-module="customers" checked> Asiakkaat
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||
<input type="checkbox" data-module="support" checked> Asiakaspalvelu
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||
<input type="checkbox" data-module="leads"> Liidit
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||
<input type="checkbox" data-module="archive" checked> Arkisto
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||
<input type="checkbox" data-module="changelog" checked> Muutosloki
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
|
||||
<input type="checkbox" data-module="settings" checked> Asetukset / API
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-primary" id="btn-save-company-settings" style="font-size:0.85rem;">Tallenna asetukset</button>
|
||||
</div>
|
||||
<!-- Postilaatikot -->
|
||||
@@ -816,7 +841,8 @@
|
||||
<label for="user-form-role">Rooli</label>
|
||||
<select id="user-form-role">
|
||||
<option value="user">Käyttäjä</option>
|
||||
<option value="admin">Ylläpitäjä</option>
|
||||
<option value="admin">Yritysadmin</option>
|
||||
<option value="superadmin">Pääkäyttäjä</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
|
||||
93
script.js
93
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) {
|
||||
<div class="detail-item"><div class="detail-label">Sähköposti</div><div class="detail-value">${detailLink(l.sahkoposti, 'email')}</div></div>
|
||||
<div class="detail-item"><div class="detail-label">Osoite</div><div class="detail-value">${detailVal([l.osoite, l.kaupunki].filter(Boolean).join(', '))}</div></div>
|
||||
<div class="detail-item"><div class="detail-label">Lisätty</div><div class="detail-value">${detailVal(l.luotu)} (${esc(l.luoja || '')})</div></div>
|
||||
${l.muokattu ? `<div class="detail-item"><div class="detail-label">Muokattu</div><div class="detail-value">${esc(l.muokattu)} (${esc(l.muokkaaja || '')})</div></div>` : ''}
|
||||
${l.muokattu ? `<div class="detail-item"><div class="detail-label">Muokattu</div><div class="detail-value">${timeAgo(l.muokattu)} (${esc(l.muokkaaja || '')})</div></div>` : ''}
|
||||
</div>
|
||||
${l.muistiinpanot ? `<div style="margin-top:1.25rem;"><div class="detail-label" style="margin-bottom:0.5rem;">MUISTIINPANOT</div><div style="white-space:pre-wrap;color:#555;background:#f8f9fb;padding:1rem;border-radius:8px;border:1px solid #e8ebf0;font-size:0.9rem;">${esc(l.muistiinpanot)}</div></div>` : ''}
|
||||
</div>`;
|
||||
@@ -943,7 +966,7 @@ async function loadUsers() {
|
||||
<td><strong>${esc(u.username)}</strong></td>
|
||||
<td>${esc(u.nimi)}</td>
|
||||
<td>${esc(u.email || '')}</td>
|
||||
<td><span class="role-badge role-${u.role}">${u.role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'}</span></td>
|
||||
<td><span class="role-badge role-${u.role}">${u.role === 'superadmin' ? 'Pääkäyttäjä' : (u.role === 'admin' ? 'Yritysadmin' : 'Käyttäjä')}</span></td>
|
||||
<td>${esc(u.luotu)}</td>
|
||||
<td class="actions-cell">
|
||||
<button onclick="editUser('${u.id}')" title="Muokkaa">✎</button>
|
||||
@@ -1178,7 +1201,7 @@ function renderTickets() {
|
||||
<td>${esc(t.mailbox_name || t.from_name || t.from_email)}</td>
|
||||
<td>${t.customer_name ? esc(t.customer_name) : '<span style="color:#ccc;">-</span>'}</td>
|
||||
<td style="text-align:center;">${lastType} ${t.message_count}</td>
|
||||
<td class="nowrap">${esc((t.updated || '').substring(0, 16))}</td>
|
||||
<td class="nowrap" title="${esc((t.updated || '').substring(0, 16))}">${timeAgo(t.updated)}</td>
|
||||
</tr>`;
|
||||
}).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 => `<tr>
|
||||
<td><code>${esc(c.id)}</code></td>
|
||||
<td><strong>${esc(c.nimi)}</strong></td>
|
||||
@@ -1973,9 +1997,12 @@ function renderCompaniesTable() {
|
||||
<td>${c.aktiivinen !== false ? '<span style="color:#22c55e;">Aktiivinen</span>' : '<span style="color:#888;">Ei aktiivinen</span>'}</td>
|
||||
<td>
|
||||
<button class="btn-link" onclick="showCompanyDetail('${c.id}')">Asetukset</button>
|
||||
<button class="btn-link" style="color:#dc2626;" onclick="deleteCompany('${c.id}','${esc(c.nimi)}')">Poista</button>
|
||||
${superAdmin ? `<button class="btn-link" style="color:#dc2626;" onclick="deleteCompany('${c.id}','${esc(c.nimi)}')">Poista</button>` : ''}
|
||||
</td>
|
||||
</tr>`).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 `<label style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0;cursor:pointer;">
|
||||
<input type="checkbox" class="company-user-cb" data-user-id="${u.id}" ${hasAccess ? 'checked' : ''} onchange="toggleCompanyUser('${u.id}','${companyId}',this.checked)">
|
||||
<strong>${esc(u.nimi || u.username)}</strong>
|
||||
<span style="color:#888;font-size:0.85rem;">(${u.username}) — ${u.role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'}</span>
|
||||
<span style="color:#888;font-size:0.85rem;">(${u.username}) — ${u.role === 'superadmin' ? 'Pääkäyttäjä' : (u.role === 'admin' ? 'Yritysadmin' : 'Käyttäjä')}</span>
|
||||
</label>`;
|
||||
}).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) {
|
||||
|
||||
Reference in New Issue
Block a user