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:
2026-03-10 18:42:07 +02:00
parent 86ffcc88de
commit a135aaaaef
5 changed files with 225 additions and 50 deletions

74
db.php
View File

@@ -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' => [],
];
}