diff --git a/.gitignore b/.gitignore
index e067634..0fbc7ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
data/backups/
data/files/
+db_config.php
diff --git a/api.php b/api.php
index f5260b3..6323d78 100644
--- a/api.php
+++ b/api.php
@@ -3,18 +3,16 @@
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.use_strict_mode', 1);
-ini_set('session.cookie_samesite', 'Strict');
+ini_set('session.cookie_samesite', 'Lax');
session_start();
+require_once __DIR__ . '/db.php';
+initDatabase();
+
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
define('DATA_DIR', __DIR__ . '/data');
-define('USERS_FILE', DATA_DIR . '/users.json');
-define('TOKENS_FILE', DATA_DIR . '/reset_tokens.json');
-define('RATE_FILE', DATA_DIR . '/login_attempts.json');
-define('CONFIG_FILE', DATA_DIR . '/config.json');
-define('COMPANIES_FILE', DATA_DIR . '/companies.json');
// Dynaaminen SITE_URL domainin mukaan
define('SITE_URL', 'https://' . ($_SERVER['HTTP_HOST'] ?? 'intra.noxus.fi'));
@@ -22,16 +20,8 @@ define('SITE_URL', 'https://' . ($_SERVER['HTTP_HOST'] ?? 'intra.noxus.fi'));
define('MAIL_FROM', 'noreply@noxus.fi');
define('MAIL_FROM_NAME', 'Noxus Intra');
-// Varmista data-kansio ja globaalit tiedostot
+// Varmista data-kansio (tiedostoja varten)
if (!file_exists(DATA_DIR)) mkdir(DATA_DIR, 0755, true);
-foreach ([USERS_FILE, TOKENS_FILE, RATE_FILE] as $f) {
- if (!file_exists($f)) file_put_contents($f, '[]');
-}
-if (!file_exists(CONFIG_FILE)) file_put_contents(CONFIG_FILE, '{}');
-if (!file_exists(COMPANIES_FILE)) file_put_contents(COMPANIES_FILE, '[]');
-
-initUsers();
-runMigration();
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
@@ -69,15 +59,6 @@ function generateToken(): string {
// ==================== MULTI-COMPANY ====================
-function loadCompanies(): array {
- if (!file_exists(COMPANIES_FILE)) return [];
- return json_decode(file_get_contents(COMPANIES_FILE), true) ?: [];
-}
-
-function saveCompanies(array $companies): void {
- file_put_contents(COMPANIES_FILE, json_encode($companies, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
function getCompanyDir(?string $companyId = null): string {
$id = $companyId ?? ($_SESSION['company_id'] ?? '');
if (empty($id) || !preg_match('/^[a-z0-9-]+$/', $id)) {
@@ -126,171 +107,10 @@ function companyFile(string $filename): string {
return getCompanyDir() . '/' . $filename;
}
-function loadCompanyConfig(): array {
- $file = companyFile('config.json');
- if (!file_exists($file)) return ['mailboxes' => [], 'ticket_rules' => []];
- return json_decode(file_get_contents($file), true) ?: ['mailboxes' => [], 'ticket_rules' => []];
-}
-
-function saveCompanyConfig(array $config): void {
- file_put_contents(companyFile('config.json'), json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
-function runMigration(): void {
- $companiesDir = DATA_DIR . '/companies';
-
- // Varmista companies.json olemassaolo ja sisältö (voi kadota/tyhjentyä git deploy:ssa)
- $companiesData = file_exists(COMPANIES_FILE) ? (json_decode(file_get_contents(COMPANIES_FILE), true) ?: []) : [];
- if (empty($companiesData)) {
- // Skannaa olemassaolevat yritys-hakemistot ja luo companies.json
- $companies = [];
- if (is_dir($companiesDir)) {
- foreach (glob($companiesDir . '/*', GLOB_ONLYDIR) as $dir) {
- $id = basename($dir);
- $companies[] = [
- 'id' => $id,
- 'nimi' => $id === 'cuitunet' ? 'CuituNet' : ucfirst($id),
- 'luotu' => date('Y-m-d H:i:s'),
- 'aktiivinen' => true,
- ];
- }
- }
- if (!empty($companies)) {
- file_put_contents(COMPANIES_FILE, json_encode($companies, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- }
- }
-
- // Varmista jokaisen yrityksen config.json
- if (is_dir($companiesDir)) {
- foreach (glob($companiesDir . '/*', GLOB_ONLYDIR) as $dir) {
- $configFile = $dir . '/config.json';
- if (!file_exists($configFile)) {
- file_put_contents($configFile, json_encode(['mailboxes' => [], 'ticket_rules' => []], JSON_PRETTY_PRINT));
- }
- }
- }
-
- // Tarkista onko vanha data olemassa juuressa (pre-multitenant)
- $oldCustomers = DATA_DIR . '/customers.json';
- if (!file_exists($oldCustomers)) return; // Ei vanhaa dataa → ei migraatiota
-
- // Vanha data löytyy juuresta → siirretään yrityksen alle
- if (!file_exists($companiesDir)) mkdir($companiesDir, 0755, true);
- $cuitunetDir = $companiesDir . '/cuitunet';
- if (!file_exists($cuitunetDir)) mkdir($cuitunetDir, 0755, true);
-
- // Luo companies.json
- $companies = [[
- 'id' => 'cuitunet',
- 'nimi' => 'CuituNet',
- 'luotu' => date('Y-m-d H:i:s'),
- 'aktiivinen' => true,
- ]];
- file_put_contents(COMPANIES_FILE, json_encode($companies, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-
- // Siirrä datatiedostot yrityksen alle
- $filesToMove = ['customers.json', 'leads.json', 'archive.json', 'tickets.json', 'changelog.json'];
- foreach ($filesToMove as $f) {
- $src = DATA_DIR . '/' . $f;
- if (file_exists($src)) {
- copy($src, $cuitunetDir . '/' . $f);
- unlink($src);
- }
- }
-
- // Siirrä tiedostokansio
- $oldFiles = DATA_DIR . '/files';
- if (is_dir($oldFiles)) {
- rename($oldFiles, $cuitunetDir . '/files');
- }
-
- // Siirrä backups-kansio
- $oldBackups = DATA_DIR . '/backups';
- if (is_dir($oldBackups)) {
- rename($oldBackups, $cuitunetDir . '/backups');
- }
-
- // Luo yrityksen config IMAP-asetuksista
- $globalConfig = json_decode(file_get_contents(CONFIG_FILE), true) ?: [];
- $companyConfig = ['mailboxes' => [], 'ticket_rules' => $globalConfig['ticket_rules'] ?? []];
-
- if (!empty($globalConfig['imap_host'])) {
- $companyConfig['mailboxes'][] = [
- 'id' => generateId(),
- 'nimi' => 'Cuitunet-asiakaspalvelu',
- 'imap_host' => $globalConfig['imap_host'],
- 'imap_port' => intval($globalConfig['imap_port'] ?? 993),
- 'imap_user' => $globalConfig['imap_user'] ?? '',
- 'imap_password' => $globalConfig['imap_password'] ?? '',
- 'imap_encryption' => $globalConfig['imap_encryption'] ?? 'ssl',
- 'smtp_from_email' => $globalConfig['imap_user'] ?? '',
- 'smtp_from_name' => 'CuituNet Asiakaspalvelu',
- 'aktiivinen' => true,
- ];
- }
-
- file_put_contents($cuitunetDir . '/config.json', json_encode($companyConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-
- // Päivitä tiketteihin mailbox_id
- $ticketsFile = $cuitunetDir . '/tickets.json';
- if (file_exists($ticketsFile)) {
- $tickets = json_decode(file_get_contents($ticketsFile), true) ?: [];
- $mbId = !empty($companyConfig['mailboxes']) ? $companyConfig['mailboxes'][0]['id'] : '';
- foreach ($tickets as &$t) {
- if (!isset($t['mailbox_id'])) $t['mailbox_id'] = $mbId;
- }
- unset($t);
- file_put_contents($ticketsFile, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- }
-
- // Lisää companies-array kaikkiin käyttäjiin
- $users = json_decode(file_get_contents(USERS_FILE), true) ?: [];
- foreach ($users as &$u) {
- if (!isset($u['companies'])) $u['companies'] = ['cuitunet'];
- }
- unset($u);
- file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-
- // Siivoa globaali config
- unset($globalConfig['imap_host'], $globalConfig['imap_port'], $globalConfig['imap_user'],
- $globalConfig['imap_password'], $globalConfig['imap_encryption'], $globalConfig['ticket_rules']);
- file_put_contents(CONFIG_FILE, json_encode($globalConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
-// ==================== RATE LIMITING ====================
-
-function checkRateLimit(string $ip): bool {
- $attempts = json_decode(file_get_contents(RATE_FILE), true) ?: [];
- $now = time();
- // Siivoa vanhat (yli 15 min)
- $attempts = array_filter($attempts, fn($a) => ($now - $a['time']) < 900);
- file_put_contents(RATE_FILE, json_encode(array_values($attempts)));
- // Laske tämän IP:n yritykset viimeisen 15 min aikana
- $ipAttempts = array_filter($attempts, fn($a) => $a['ip'] === $ip);
- return count($ipAttempts) < 10; // Max 10 yritystä / 15 min
-}
-
-function recordLoginAttempt(string $ip): void {
- $attempts = json_decode(file_get_contents(RATE_FILE), true) ?: [];
- $attempts[] = ['ip' => $ip, 'time' => time()];
- file_put_contents(RATE_FILE, json_encode($attempts));
-}
-
function getClientIp(): string {
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
-// ==================== CONFIG ====================
-
-function loadConfig(): array {
- if (!file_exists(CONFIG_FILE)) return [];
- return json_decode(file_get_contents(CONFIG_FILE), true) ?: [];
-}
-
-function saveConfig(array $config): void {
- file_put_contents(CONFIG_FILE, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
function normalizeAddress(string $addr): string {
$addr = strtolower(trim($addr));
$addr = preg_replace('/\s+/', ' ', $addr);
@@ -674,46 +494,7 @@ class ImapClient {
}
}
-// ==================== TICKETS ====================
-
-function loadTickets(): array {
- $file = companyFile('tickets.json');
- if (!file_exists($file)) { file_put_contents($file, '[]'); return []; }
- return json_decode(file_get_contents($file), true) ?: [];
-}
-
-function saveTickets(array $tickets): void {
- file_put_contents(companyFile('tickets.json'), json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
-function findTicketByMessageId(array $tickets, string $messageId): ?int {
- foreach ($tickets as $i => $t) {
- if ($t['message_id'] === $messageId) return $i;
- foreach ($t['messages'] ?? [] as $m) {
- if (($m['message_id'] ?? '') === $messageId) return $i;
- }
- }
- return null;
-}
-
-function findTicketByReferences(array $tickets, string $inReplyTo, string $references): ?int {
- // Check In-Reply-To header
- if ($inReplyTo) {
- $idx = findTicketByMessageId($tickets, $inReplyTo);
- if ($idx !== null) return $idx;
- }
- // Check References header
- if ($references) {
- $refs = preg_split('/\s+/', $references);
- foreach ($refs as $ref) {
- $ref = trim($ref);
- if (!$ref) continue;
- $idx = findTicketByMessageId($tickets, $ref);
- if ($idx !== null) return $idx;
- }
- }
- return null;
-}
+// ==================== TICKETS HELPER ====================
function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null): bool {
$fromEmail = $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? MAIL_FROM;
@@ -730,142 +511,6 @@ function sendTicketMail(string $to, string $subject, string $body, string $inRep
return mail($to, $subject, $body, $headers, '-f ' . $fromEmail);
}
-// ==================== USERS ====================
-
-function initUsers(): void {
- $users = json_decode(file_get_contents(USERS_FILE), true) ?: [];
- if (empty($users)) {
- $users[] = [
- 'id' => generateId(),
- 'username' => 'admin',
- 'password_hash' => password_hash('cuitunet2024', PASSWORD_DEFAULT),
- 'nimi' => 'Ylläpitäjä',
- 'email' => '',
- 'role' => 'admin',
- 'companies' => [],
- 'luotu' => date('Y-m-d H:i:s'),
- ];
- file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- }
-}
-
-function loadUsers(): array {
- return json_decode(file_get_contents(USERS_FILE), true) ?: [];
-}
-
-function saveUsers(array $users): void {
- file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
-// ==================== RESET TOKENS ====================
-
-function saveToken(string $userId, string $token): void {
- $tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
- // Poista vanhat tokenit tälle käyttäjälle
- $tokens = array_filter($tokens, fn($t) => $t['user_id'] !== $userId);
- $tokens[] = [
- 'user_id' => $userId,
- 'token' => hash('sha256', $token),
- 'expires' => time() + 3600, // 1 tunti
- ];
- file_put_contents(TOKENS_FILE, json_encode(array_values($tokens)));
-}
-
-function validateToken(string $token): ?string {
- $tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
- $hashed = hash('sha256', $token);
- $now = time();
- foreach ($tokens as $t) {
- if ($t['token'] === $hashed && $t['expires'] > $now) {
- return $t['user_id'];
- }
- }
- return null;
-}
-
-function removeToken(string $token): void {
- $tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
- $hashed = hash('sha256', $token);
- $tokens = array_filter($tokens, fn($t) => $t['token'] !== $hashed);
- file_put_contents(TOKENS_FILE, json_encode(array_values($tokens)));
-}
-
-// ==================== CHANGELOG ====================
-
-function addLog(string $action, string $customerId = '', string $customerName = '', string $details = ''): void {
- // Jos company-kontekstia ei ole (esim. globaalit asetukset), ohitetaan
- if (empty($_SESSION['company_id'])) return;
- $file = companyFile('changelog.json');
- if (!file_exists($file)) file_put_contents($file, '[]');
- $log = json_decode(file_get_contents($file), true) ?: [];
- array_unshift($log, [
- 'id' => generateId(),
- 'timestamp' => date('Y-m-d H:i:s'),
- 'user' => currentUser(),
- 'action' => $action,
- 'customer_id' => $customerId,
- 'customer_name' => $customerName,
- 'details' => $details,
- ]);
- if (count($log) > 500) $log = array_slice($log, 0, 500);
- file_put_contents($file, json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
-// ==================== CUSTOMERS ====================
-
-function loadCustomers(): array {
- $file = companyFile('customers.json');
- if (!file_exists($file)) { file_put_contents($file, '[]'); return []; }
- $data = file_get_contents($file);
- $customers = json_decode($data, true) ?: [];
- $migrated = false;
- foreach ($customers as &$c) {
- if (!isset($c['liittymat'])) {
- $c['liittymat'] = [[
- 'asennusosoite' => $c['asennusosoite'] ?? '',
- 'postinumero' => $c['postinumero'] ?? '',
- 'kaupunki' => $c['kaupunki'] ?? '',
- 'liittymanopeus' => $c['liittymanopeus'] ?? '',
- 'hinta' => floatval($c['hinta'] ?? 0),
- 'sopimuskausi' => '',
- 'alkupvm' => '',
- ]];
- unset($c['asennusosoite'], $c['postinumero'], $c['kaupunki'], $c['liittymanopeus'], $c['hinta']);
- $migrated = true;
- }
- }
- unset($c);
- if ($migrated) {
- file_put_contents($file, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- }
- return $customers;
-}
-
-function saveCustomers(array $customers): void {
- $file = companyFile('customers.json');
- if (file_exists($file) && filesize($file) > 2) {
- $backupDir = getCompanyDir() . '/backups';
- if (!file_exists($backupDir)) mkdir($backupDir, 0755, true);
- copy($file, $backupDir . '/customers_' . date('Y-m-d_His') . '.json');
- $backups = glob($backupDir . '/customers_*.json');
- if (count($backups) > 30) {
- sort($backups);
- array_map('unlink', array_slice($backups, 0, count($backups) - 30));
- }
- }
- file_put_contents($file, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
-function loadArchive(): array {
- $file = companyFile('archive.json');
- if (!file_exists($file)) { file_put_contents($file, '[]'); return []; }
- return json_decode(file_get_contents($file), true) ?: [];
-}
-
-function saveArchive(array $archive): void {
- file_put_contents(companyFile('archive.json'), json_encode($archive, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-}
-
function parseLiittymat(array $input): array {
$liittymat = [];
foreach (($input['liittymat'] ?? []) as $l) {
@@ -899,18 +544,7 @@ switch ($action) {
}
// Etsi yritys jonka API-avain täsmää
- $matchedCompany = null;
- $allCompanies = loadCompanies();
- foreach ($allCompanies as $comp) {
- $confFile = DATA_DIR . '/companies/' . $comp['id'] . '/config.json';
- if (!file_exists($confFile)) continue;
- $compConf = json_decode(file_get_contents($confFile), true) ?: [];
- if (!empty($compConf['api_key']) && $compConf['api_key'] === $providedKey) {
- $matchedCompany = $comp;
- $matchedConfig = $compConf;
- break;
- }
- }
+ $matchedCompany = dbGetCompanyByApiKey($providedKey);
if (!$matchedCompany) {
http_response_code(403);
@@ -919,7 +553,7 @@ switch ($action) {
}
// CORS - yrityskohtaiset originit
- $allowedOrigins = $matchedConfig['cors_origins'] ?? [];
+ $allowedOrigins = dbGetCompanyCorsOrigins($matchedCompany['id']);
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
@@ -940,22 +574,18 @@ switch ($action) {
}
// Hae VAIN tämän yrityksen asiakkaista
- $compDir = DATA_DIR . '/companies/' . $matchedCompany['id'];
- $custFile = $compDir . '/customers.json';
+ $customers = dbLoadCustomers($matchedCompany['id']);
$found = false;
- if (file_exists($custFile)) {
- $customers = json_decode(file_get_contents($custFile), true) ?: [];
- foreach ($customers as $c) {
- foreach ($c['liittymat'] ?? [] as $l) {
- $addr = normalizeAddress($l['asennusosoite'] ?? '');
- $zip = trim($l['postinumero'] ?? '');
- $city = strtolower(trim($l['kaupunki'] ?? ''));
- if ($zip === $queryPostinumero && $city === $queryKaupunki) {
- if (!empty($addr) && !empty($queryOsoite)) {
- if (strpos($addr, $queryOsoite) !== false || strpos($queryOsoite, $addr) !== false) {
- $found = true;
- break 2;
- }
+ foreach ($customers as $c) {
+ foreach ($c['liittymat'] ?? [] as $l) {
+ $addr = normalizeAddress($l['asennusosoite'] ?? '');
+ $zip = trim($l['postinumero'] ?? '');
+ $city = strtolower(trim($l['kaupunki'] ?? ''));
+ if ($zip === $queryPostinumero && $city === $queryKaupunki) {
+ if (!empty($addr) && !empty($queryOsoite)) {
+ if (strpos($addr, $queryOsoite) !== false || strpos($queryOsoite, $addr) !== false) {
+ $found = true;
+ break 2;
}
}
}
@@ -968,44 +598,42 @@ switch ($action) {
// ---------- CONFIG (admin, yrityskohtainen) ----------
case 'config':
requireAdmin();
- requireCompany();
- $compConf = loadCompanyConfig();
+ $companyId = requireCompany();
echo json_encode([
- 'api_key' => $compConf['api_key'] ?? '',
- 'cors_origins' => $compConf['cors_origins'] ?? [],
+ 'api_key' => dbGetCompanyApiKey($companyId),
+ 'cors_origins' => dbGetCompanyCorsOrigins($companyId),
]);
break;
case 'config_update':
requireAdmin();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
- $compConf = loadCompanyConfig();
- if (isset($input['api_key'])) $compConf['api_key'] = trim($input['api_key']);
+ if (isset($input['api_key'])) {
+ dbSetCompanyApiKey($companyId, trim($input['api_key']));
+ }
if (isset($input['cors_origins'])) {
$origins = array_filter(array_map('trim', explode("\n", $input['cors_origins'])));
- $compConf['cors_origins'] = array_values($origins);
+ dbSetCompanyCorsOrigins($companyId, array_values($origins));
}
- saveCompanyConfig($compConf);
- addLog('config_update', '', '', 'Päivitti API-asetukset');
+ dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Päivitti API-asetukset');
echo json_encode([
- 'api_key' => $compConf['api_key'] ?? '',
- 'cors_origins' => $compConf['cors_origins'] ?? [],
+ 'api_key' => dbGetCompanyApiKey($companyId),
+ 'cors_origins' => dbGetCompanyCorsOrigins($companyId),
]);
break;
case 'generate_api_key':
requireAdmin();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
- $compConf = loadCompanyConfig();
- $compConf['api_key'] = bin2hex(random_bytes(16));
- saveCompanyConfig($compConf);
- addLog('config_update', '', '', 'Generoi uuden API-avaimen');
+ $newApiKey = bin2hex(random_bytes(16));
+ dbSetCompanyApiKey($companyId, $newApiKey);
+ dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Generoi uuden API-avaimen');
echo json_encode([
- 'api_key' => $compConf['api_key'] ?? '',
- 'cors_origins' => $compConf['cors_origins'] ?? [],
+ 'api_key' => $newApiKey,
+ 'cors_origins' => dbGetCompanyCorsOrigins($companyId),
]);
break;
@@ -1019,43 +647,8 @@ switch ($action) {
// ---------- BRANDING (julkinen) ----------
case 'branding':
- $host = $_SERVER['HTTP_HOST'] ?? '';
- // Stripaa portti pois (localhost:3001 → localhost)
- $host = strtolower(explode(':', $host)[0]);
- $companies = loadCompanies();
- $matchedCompany = null;
- foreach ($companies as $comp) {
- $domains = $comp['domains'] ?? [];
- foreach ($domains as $d) {
- if (strtolower(trim($d)) === strtolower($host)) {
- $matchedCompany = $comp;
- break 2;
- }
- }
- }
- if ($matchedCompany) {
- $logoUrl = !empty($matchedCompany['logo_file'])
- ? "api.php?action=company_logo&company_id=" . urlencode($matchedCompany['id'])
- : '';
- echo json_encode([
- 'found' => true,
- 'company_id' => $matchedCompany['id'],
- 'nimi' => $matchedCompany['nimi'],
- 'primary_color' => $matchedCompany['primary_color'] ?? '#0f3460',
- 'subtitle' => $matchedCompany['subtitle'] ?? '',
- 'logo_url' => $logoUrl,
- ]);
- } else {
- // Noxus Intra -oletusbrändäys
- echo json_encode([
- 'found' => false,
- 'company_id' => '',
- 'nimi' => 'Noxus Intra',
- 'primary_color' => '#0f3460',
- 'subtitle' => 'Hallintapaneeli',
- 'logo_url' => '',
- ]);
- }
+ $host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
+ echo json_encode(dbGetBranding($host));
break;
case 'company_logo':
@@ -1065,7 +658,7 @@ switch ($action) {
echo json_encode(['error' => 'Virheellinen company_id']);
break;
}
- $companies = loadCompanies();
+ $companies = dbLoadCompanies();
$logoFile = '';
foreach ($companies as $comp) {
if ($comp['id'] === $companyId) {
@@ -1133,19 +726,18 @@ switch ($action) {
break;
}
// Poista vanha logo
- $companies = loadCompanies();
- foreach ($companies as &$comp) {
+ $companies = dbLoadCompanies();
+ foreach ($companies as $comp) {
if ($comp['id'] === $companyId) {
$oldLogo = $comp['logo_file'] ?? '';
if ($oldLogo && $oldLogo !== $newFilename && file_exists($compDir . '/' . $oldLogo)) {
unlink($compDir . '/' . $oldLogo);
}
$comp['logo_file'] = $newFilename;
+ dbSaveCompany($comp);
break;
}
}
- unset($comp);
- saveCompanies($companies);
move_uploaded_file($file['tmp_name'], $compDir . '/' . $newFilename);
echo json_encode([
'success' => true,
@@ -1158,7 +750,7 @@ switch ($action) {
case 'login':
if ($method !== 'POST') break;
$ip = getClientIp();
- if (!checkRateLimit($ip)) {
+ if (!dbCheckRateLimit($ip)) {
http_response_code(429);
echo json_encode(['error' => 'Liian monta kirjautumisyritystä. Yritä uudelleen 15 minuutin kuluttua.']);
break;
@@ -1167,7 +759,7 @@ switch ($action) {
// Captcha-tarkistus
$captchaAnswer = intval($input['captcha'] ?? 0);
if (!isset($_SESSION['captcha_answer']) || $captchaAnswer !== $_SESSION['captcha_answer']) {
- recordLoginAttempt($ip);
+ dbRecordLoginAttempt($ip);
http_response_code(400);
echo json_encode(['error' => 'Virheellinen captcha-vastaus']);
unset($_SESSION['captcha_answer']);
@@ -1176,59 +768,45 @@ switch ($action) {
unset($_SESSION['captcha_answer']);
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
- $users = loadUsers();
- $found = false;
- foreach ($users as $u) {
- if ($u['username'] === $username && password_verify($password, $u['password_hash'])) {
- session_regenerate_id(true);
- $_SESSION['user_id'] = $u['id'];
- $_SESSION['username'] = $u['username'];
- $_SESSION['nimi'] = $u['nimi'];
- $_SESSION['role'] = $u['role'];
- // Multi-company: aseta käyttäjän yritykset sessioon
- $userCompanies = $u['companies'] ?? [];
- $_SESSION['companies'] = $userCompanies;
- // Domain-pohjainen oletusyritys
- $host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
- $domainCompanyId = '';
- $allComps = loadCompanies();
- foreach ($allComps as $dc) {
- foreach ($dc['domains'] ?? [] as $d) {
- if (strtolower(trim($d)) === strtolower($host)) {
- $domainCompanyId = $dc['id'];
- break 2;
- }
- }
- }
- // Jos domain matchaa ja käyttäjällä on oikeus → käytä sitä
- if ($domainCompanyId && in_array($domainCompanyId, $userCompanies)) {
- $_SESSION['company_id'] = $domainCompanyId;
- } else {
- $_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
- }
- // Hae yritysten nimet
- $allCompanies = loadCompanies();
- $companyList = [];
- foreach ($allCompanies as $comp) {
- if (in_array($comp['id'], $userCompanies)) {
- $companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
- }
- }
- echo json_encode([
- 'success' => true,
- 'username' => $u['username'],
- 'nimi' => $u['nimi'],
- 'role' => $u['role'],
- 'companies' => $companyList,
- 'company_id' => $_SESSION['company_id'],
- 'signatures' => $u['signatures'] ?? [],
- ]);
- $found = true;
- break;
+ $u = dbGetUserByUsername($username);
+ if ($u && password_verify($password, $u['password_hash'])) {
+ session_regenerate_id(true);
+ $_SESSION['user_id'] = $u['id'];
+ $_SESSION['username'] = $u['username'];
+ $_SESSION['nimi'] = $u['nimi'];
+ $_SESSION['role'] = $u['role'];
+ // Multi-company: aseta käyttäjän yritykset sessioon
+ $userCompanies = $u['companies'] ?? [];
+ $_SESSION['companies'] = $userCompanies;
+ // Domain-pohjainen oletusyritys
+ $host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
+ $domainCompany = dbGetCompanyByDomain($host);
+ $domainCompanyId = $domainCompany ? $domainCompany['id'] : '';
+ // Jos domain matchaa ja käyttäjällä on oikeus -> käytä sitä
+ if ($domainCompanyId && in_array($domainCompanyId, $userCompanies)) {
+ $_SESSION['company_id'] = $domainCompanyId;
+ } else {
+ $_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
}
- }
- if (!$found) {
- recordLoginAttempt($ip);
+ // Hae yritysten nimet
+ $allCompanies = dbLoadCompanies();
+ $companyList = [];
+ foreach ($allCompanies as $comp) {
+ if (in_array($comp['id'], $userCompanies)) {
+ $companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
+ }
+ }
+ echo json_encode([
+ 'success' => true,
+ 'username' => $u['username'],
+ 'nimi' => $u['nimi'],
+ 'role' => $u['role'],
+ 'companies' => $companyList,
+ 'company_id' => $_SESSION['company_id'],
+ 'signatures' => $u['signatures'] ?? [],
+ ]);
+ } else {
+ dbRecordLoginAttempt($ip);
http_response_code(401);
echo json_encode(['error' => 'Väärä käyttäjätunnus tai salasana']);
}
@@ -1241,21 +819,18 @@ switch ($action) {
case 'check_auth':
if (isset($_SESSION['user_id'])) {
- // Synkronoi aina tuoreet yritysoikeudet users.json:sta sessioon
- $users = loadUsers();
- foreach ($users as $u) {
- if ($u['id'] === $_SESSION['user_id']) {
- $_SESSION['companies'] = $u['companies'] ?? [];
- // Varmista aktiivinen yritys on sallittu
- if (!in_array($_SESSION['company_id'] ?? '', $_SESSION['companies'])) {
- $_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : '';
- }
- break;
+ // Synkronoi aina tuoreet yritysoikeudet tietokannasta sessioon
+ $u = dbGetUser($_SESSION['user_id']);
+ if ($u) {
+ $_SESSION['companies'] = $u['companies'] ?? [];
+ // Varmista aktiivinen yritys on sallittu
+ if (!in_array($_SESSION['company_id'] ?? '', $_SESSION['companies'])) {
+ $_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : '';
}
}
// Hae yritysten nimet
$userCompanyIds = $_SESSION['companies'] ?? [];
- $allCompanies = loadCompanies();
+ $allCompanies = dbLoadCompanies();
$companyList = [];
foreach ($allCompanies as $comp) {
if (in_array($comp['id'], $userCompanyIds)) {
@@ -1263,13 +838,7 @@ switch ($action) {
}
}
// Hae allekirjoitukset
- $userSignatures = [];
- foreach ($users as $uu) {
- if ($uu['id'] === $_SESSION['user_id']) {
- $userSignatures = $uu['signatures'] ?? [];
- break;
- }
- }
+ $userSignatures = $u ? ($u['signatures'] ?? []) : [];
// Brändäystiedot aktiivisesta yrityksestä
$branding = ['primary_color' => '#0f3460', 'subtitle' => '', 'logo_url' => '', 'company_nimi' => ''];
$activeCompanyId = $_SESSION['company_id'] ?? '';
@@ -1304,23 +873,19 @@ switch ($action) {
case 'password_reset_request':
if ($method !== 'POST') break;
$ip = getClientIp();
- if (!checkRateLimit($ip)) {
+ if (!dbCheckRateLimit($ip)) {
http_response_code(429);
echo json_encode(['error' => 'Liian monta yritystä. Yritä uudelleen myöhemmin.']);
break;
}
- recordLoginAttempt($ip);
+ dbRecordLoginAttempt($ip);
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
- $users = loadUsers();
- $user = null;
- foreach ($users as $u) {
- if ($u['username'] === $username) { $user = $u; break; }
- }
+ $user = dbGetUserByUsername($username);
// Palauta aina sama viesti (ei paljasta onko tunnus olemassa)
if ($user && !empty($user['email'])) {
$token = generateToken();
- saveToken($user['id'], $token);
+ dbSaveToken($user['id'], $token);
$resetUrl = SITE_URL . '/?reset=' . $token;
$html = '
';
$html .= '
Noxus Intra
';
@@ -1344,35 +909,31 @@ switch ($action) {
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
break;
}
- $userId = validateToken($token);
+ $userId = dbValidateToken($token);
if (!$userId) {
http_response_code(400);
echo json_encode(['error' => 'Palautuslinkki on vanhentunut tai virheellinen']);
break;
}
- $users = loadUsers();
- foreach ($users as &$u) {
- if ($u['id'] === $userId) {
- $u['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
- break;
- }
+ $user = dbGetUser($userId);
+ if ($user) {
+ $user['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
+ dbSaveUser($user);
}
- unset($u);
- saveUsers($users);
- removeToken($token);
+ dbRemoveToken($token);
echo json_encode(['success' => true, 'message' => 'Salasana vaihdettu onnistuneesti']);
break;
case 'validate_reset_token':
$token = $_GET['token'] ?? '';
- $userId = validateToken($token);
+ $userId = dbValidateToken($token);
echo json_encode(['valid' => $userId !== null]);
break;
// ---------- USERS ----------
case 'users':
requireAdmin();
- $users = loadUsers();
+ $users = dbLoadUsers();
$safe = array_map(function($u) {
unset($u['password_hash']);
return $u;
@@ -1399,17 +960,15 @@ switch ($action) {
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
break;
}
- $users = loadUsers();
- foreach ($users as $u) {
- if ($u['username'] === $username) {
- http_response_code(400);
- echo json_encode(['error' => 'Käyttäjätunnus on jo käytössä']);
- break 2;
- }
+ $existingUser = dbGetUserByUsername($username);
+ if ($existingUser) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Käyttäjätunnus on jo käytössä']);
+ break;
}
$companies = $input['companies'] ?? [];
// Validoi yritys-IDt
- $allCompanies = loadCompanies();
+ $allCompanies = dbLoadCompanies();
$validIds = array_column($allCompanies, 'id');
$companies = array_values(array_filter($companies, fn($c) => in_array($c, $validIds)));
$signatures = [];
@@ -1429,9 +988,9 @@ switch ($action) {
'signatures' => $signatures,
'luotu' => date('Y-m-d H:i:s'),
];
- $users[] = $newUser;
- saveUsers($users);
- addLog('user_create', '', '', "Lisäsi käyttäjän: {$username} ({$role})");
+ dbSaveUser($newUser);
+ $companyId = $_SESSION['company_id'] ?? '';
+ dbAddLog($companyId, currentUser(), 'user_create', '', '', "Lisäsi käyttäjän: {$username} ({$role})");
unset($newUser['password_hash']);
echo json_encode($newUser);
break;
@@ -1441,53 +1000,46 @@ switch ($action) {
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $users = loadUsers();
- $found = false;
- foreach ($users as &$u) {
- if ($u['id'] === $id) {
- 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['companies'])) {
- $allCompanies = loadCompanies();
- $validIds = array_column($allCompanies, 'id');
- $u['companies'] = array_values(array_filter($input['companies'], fn($c) => in_array($c, $validIds)));
- }
- if (!empty($input['password'])) {
- $u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT);
- }
- if (isset($input['signatures']) && is_array($input['signatures'])) {
- $sigs = [];
- foreach ($input['signatures'] as $mbId => $sig) {
- $sigs[(string)$mbId] = (string)$sig;
- }
- $u['signatures'] = $sigs;
- }
- $found = true;
- addLog('user_update', '', '', "Muokkasi käyttäjää: {$u['username']}");
- // Päivitä sessio jos muokattiin kirjautunutta käyttäjää
- if ($u['id'] === $_SESSION['user_id']) {
- $_SESSION['companies'] = $u['companies'] ?? [];
- if (!empty($u['companies']) && !in_array($_SESSION['company_id'] ?? '', $u['companies'])) {
- $_SESSION['company_id'] = $u['companies'][0];
- }
- if (empty($u['companies'])) {
- $_SESSION['company_id'] = '';
- }
- }
- $safe = $u;
- unset($safe['password_hash']);
- echo json_encode($safe);
- break;
- }
- }
- unset($u);
- if (!$found) {
+ $u = dbGetUser($id);
+ if (!$u) {
http_response_code(404);
echo json_encode(['error' => 'Käyttäjää ei löydy']);
break;
}
- saveUsers($users);
+ 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['companies'])) {
+ $allCompanies = dbLoadCompanies();
+ $validIds = array_column($allCompanies, 'id');
+ $u['companies'] = array_values(array_filter($input['companies'], fn($c) => in_array($c, $validIds)));
+ }
+ if (!empty($input['password'])) {
+ $u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT);
+ }
+ if (isset($input['signatures']) && is_array($input['signatures'])) {
+ $sigs = [];
+ foreach ($input['signatures'] as $mbId => $sig) {
+ $sigs[(string)$mbId] = (string)$sig;
+ }
+ $u['signatures'] = $sigs;
+ }
+ dbSaveUser($u);
+ $companyId = $_SESSION['company_id'] ?? '';
+ dbAddLog($companyId, currentUser(), 'user_update', '', '', "Muokkasi käyttäjää: {$u['username']}");
+ // Päivitä sessio jos muokattiin kirjautunutta käyttäjää
+ if ($u['id'] === $_SESSION['user_id']) {
+ $_SESSION['companies'] = $u['companies'] ?? [];
+ if (!empty($u['companies']) && !in_array($_SESSION['company_id'] ?? '', $u['companies'])) {
+ $_SESSION['company_id'] = $u['companies'][0];
+ }
+ if (empty($u['companies'])) {
+ $_SESSION['company_id'] = '';
+ }
+ }
+ $safe = $u;
+ unset($safe['password_hash']);
+ echo json_encode($safe);
break;
case 'user_delete':
@@ -1500,43 +1052,35 @@ switch ($action) {
echo json_encode(['error' => 'Et voi poistaa itseäsi']);
break;
}
- $users = loadUsers();
- $deleted = null;
- foreach ($users as $u) {
- if ($u['id'] === $id) { $deleted = $u; break; }
- }
- $users = array_values(array_filter($users, fn($u) => $u['id'] !== $id));
- saveUsers($users);
- if ($deleted) addLog('user_delete', '', '', "Poisti käyttäjän: {$deleted['username']}");
+ $deleted = dbGetUser($id);
+ dbDeleteUser($id);
+ $companyId = $_SESSION['company_id'] ?? '';
+ if ($deleted) dbAddLog($companyId, currentUser(), 'user_delete', '', '', "Poisti käyttäjän: {$deleted['username']}");
echo json_encode(['success' => true]);
break;
// ---------- CHANGELOG ----------
case 'changelog':
requireAuth();
- requireCompany();
- $logFile = companyFile('changelog.json');
- if (!file_exists($logFile)) file_put_contents($logFile, '[]');
- $log = json_decode(file_get_contents($logFile), true) ?: [];
+ $companyId = requireCompany();
$limit = intval($_GET['limit'] ?? 100);
- echo json_encode(array_slice($log, 0, $limit));
+ echo json_encode(dbLoadChangelog($companyId, $limit));
break;
// ---------- CUSTOMERS ----------
case 'customers':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method === 'GET') {
- echo json_encode(loadCustomers());
+ echo json_encode(dbLoadCustomers($companyId));
}
break;
case 'customer':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
- $customers = loadCustomers();
$customer = [
'id' => generateId(),
'yritys' => trim($input['yritys'] ?? ''),
@@ -1559,22 +1103,21 @@ switch ($action) {
echo json_encode(['error' => 'Yrityksen nimi vaaditaan']);
break;
}
- $customers[] = $customer;
- saveCustomers($customers);
- addLog('customer_create', $customer['id'], $customer['yritys'], 'Lisäsi asiakkaan');
+ dbSaveCustomer($companyId, $customer);
+ dbAddLog($companyId, currentUser(), 'customer_create', $customer['id'], $customer['yritys'], 'Lisäsi asiakkaan');
echo json_encode($customer);
}
break;
case 'customer_update':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $customers = loadCustomers();
+ $customers = dbLoadCustomers($companyId);
$found = false;
- foreach ($customers as &$c) {
+ foreach ($customers as $c) {
if ($c['id'] === $id) {
$changes = [];
$fields = ['yritys','yhteyshenkilö','puhelin','sahkoposti','laskutusosoite','laskutuspostinumero','laskutuskaupunki','laskutussahkoposti','elaskuosoite','elaskuvalittaja','ytunnus','lisatiedot'];
@@ -1591,45 +1134,40 @@ switch ($action) {
$changes[] = 'liittymat';
}
$c['muokattu'] = date('Y-m-d H:i:s');
+ $c['muokkaaja'] = currentUser();
$found = true;
- addLog('customer_update', $c['id'], $c['yritys'], 'Muokkasi: ' . implode(', ', $changes));
+ dbSaveCustomer($companyId, $c);
+ dbAddLog($companyId, currentUser(), 'customer_update', $c['id'], $c['yritys'], 'Muokkasi: ' . implode(', ', $changes));
echo json_encode($c);
break;
}
}
- unset($c);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Asiakasta ei löydy']);
- break;
}
- saveCustomers($customers);
break;
case 'customer_delete':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $customers = loadCustomers();
+ $customers = dbLoadCustomers($companyId);
$archived = null;
- $remaining = [];
foreach ($customers as $c) {
if ($c['id'] === $id) {
$c['arkistoitu'] = date('Y-m-d H:i:s');
$c['arkistoija'] = currentUser();
$archived = $c;
- } else {
- $remaining[] = $c;
+ break;
}
}
if ($archived) {
- $archive = loadArchive();
- $archive[] = $archived;
- saveArchive($archive);
- saveCustomers($remaining);
- addLog('customer_archive', $archived['id'], $archived['yritys'], 'Arkistoi asiakkaan');
+ dbArchiveCustomer($companyId, $archived);
+ dbDeleteCustomer($id);
+ dbAddLog($companyId, currentUser(), 'customer_archive', $archived['id'], $archived['yritys'], 'Arkistoi asiakkaan');
}
echo json_encode(['success' => true]);
break;
@@ -1637,72 +1175,57 @@ switch ($action) {
// ---------- ARCHIVE ----------
case 'archived_customers':
requireAuth();
- requireCompany();
- echo json_encode(loadArchive());
+ $companyId = requireCompany();
+ echo json_encode(dbLoadArchive($companyId));
break;
case 'customer_restore':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $archive = loadArchive();
- $restored = null;
- $remaining = [];
- foreach ($archive as $c) {
- if ($c['id'] === $id) {
- unset($c['arkistoitu'], $c['arkistoija']);
- $restored = $c;
- } else {
- $remaining[] = $c;
- }
- }
+ $restored = dbRestoreArchive($id);
if ($restored) {
- $customers = loadCustomers();
- $customers[] = $restored;
- saveCustomers($customers);
- saveArchive($remaining);
- addLog('customer_restore', $restored['id'], $restored['yritys'], 'Palautti asiakkaan arkistosta');
+ unset($restored['arkistoitu'], $restored['arkistoija'], $restored['archived_at']);
+ dbSaveCustomer($companyId, $restored);
+ dbAddLog($companyId, currentUser(), 'customer_restore', $restored['id'], $restored['yritys'] ?? '', 'Palautti asiakkaan arkistosta');
}
echo json_encode(['success' => true]);
break;
case 'customer_permanent_delete':
requireAdmin();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $archive = loadArchive();
+ // Hae arkistoidun tiedot ennen poistoa
+ $archive = dbLoadArchive($companyId);
$deleted = null;
foreach ($archive as $c) {
if ($c['id'] === $id) { $deleted = $c; break; }
}
- $archive = array_values(array_filter($archive, fn($c) => $c['id'] !== $id));
- saveArchive($archive);
+ dbDeleteArchive($id);
$filesDir = getCompanyDir() . '/files/' . $id;
if (is_dir($filesDir)) {
array_map('unlink', glob($filesDir . '/*'));
rmdir($filesDir);
}
- if ($deleted) addLog('customer_permanent_delete', $id, $deleted['yritys'] ?? '', 'Poisti pysyvästi');
+ if ($deleted) dbAddLog($companyId, currentUser(), 'customer_permanent_delete', $id, $deleted['yritys'] ?? '', 'Poisti pysyvästi');
echo json_encode(['success' => true]);
break;
// ---------- LEADS ----------
case 'leads':
requireAuth();
- requireCompany();
- $leadsFile = companyFile('leads.json');
- if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
- $leads = json_decode(file_get_contents($leadsFile), true) ?: [];
- echo json_encode($leads);
+ $companyId = requireCompany();
+ echo json_encode(dbLoadLeads($companyId));
break;
case 'lead_create':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$lead = [
@@ -1723,26 +1246,20 @@ switch ($action) {
echo json_encode(['error' => 'Yrityksen nimi vaaditaan']);
break;
}
- $leadsFile = companyFile('leads.json');
- if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
- $leads = json_decode(file_get_contents($leadsFile), true) ?: [];
- $leads[] = $lead;
- file_put_contents($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- addLog('lead_create', $lead['id'], $lead['yritys'], 'Lisäsi liidin');
+ dbSaveLead($companyId, $lead);
+ dbAddLog($companyId, currentUser(), 'lead_create', $lead['id'], $lead['yritys'], 'Lisäsi liidin');
echo json_encode($lead);
break;
case 'lead_update':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $leadsFile = companyFile('leads.json');
- if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
- $leads = json_decode(file_get_contents($leadsFile), true) ?: [];
+ $leads = dbLoadLeads($companyId);
$found = false;
- foreach ($leads as &$l) {
+ foreach ($leads as $l) {
if ($l['id'] === $id) {
$fields = ['yritys','yhteyshenkilo','puhelin','sahkoposti','osoite','kaupunki','tila','muistiinpanot'];
foreach ($fields as $f) {
@@ -1751,48 +1268,41 @@ switch ($action) {
$l['muokattu'] = date('Y-m-d H:i:s');
$l['muokkaaja'] = currentUser();
$found = true;
- addLog('lead_update', $l['id'], $l['yritys'], 'Muokkasi liidiä');
+ dbSaveLead($companyId, $l);
+ dbAddLog($companyId, currentUser(), 'lead_update', $l['id'], $l['yritys'], 'Muokkasi liidiä');
echo json_encode($l);
break;
}
}
- unset($l);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Liidiä ei löydy']);
- break;
}
- file_put_contents($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
break;
case 'lead_delete':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $leadsFile = companyFile('leads.json');
- if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
- $leads = json_decode(file_get_contents($leadsFile), true) ?: [];
+ $leads = dbLoadLeads($companyId);
$deleted = null;
foreach ($leads as $l) {
if ($l['id'] === $id) { $deleted = $l; break; }
}
- $leads = array_values(array_filter($leads, fn($l) => $l['id'] !== $id));
- file_put_contents($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- if ($deleted) addLog('lead_delete', $id, $deleted['yritys'] ?? '', 'Poisti liidin');
+ dbDeleteLead($id);
+ if ($deleted) dbAddLog($companyId, currentUser(), 'lead_delete', $id, $deleted['yritys'] ?? '', 'Poisti liidin');
echo json_encode(['success' => true]);
break;
case 'lead_to_customer':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $leadsFile = companyFile('leads.json');
- if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]');
- $leads = json_decode(file_get_contents($leadsFile), true) ?: [];
+ $leads = dbLoadLeads($companyId);
$lead = null;
foreach ($leads as $l) {
if ($l['id'] === $id) { $lead = $l; break; }
@@ -1820,13 +1330,10 @@ switch ($action) {
'liittymat' => [['asennusosoite' => $lead['osoite'] ?? '', 'postinumero' => '', 'kaupunki' => $lead['kaupunki'] ?? '', 'liittymanopeus' => '', 'hinta' => 0, 'sopimuskausi' => '', 'alkupvm' => '']],
'luotu' => date('Y-m-d H:i:s'),
];
- $customers = loadCustomers();
- $customers[] = $customer;
- saveCustomers($customers);
+ dbSaveCustomer($companyId, $customer);
// Poista liidi
- $leads = array_values(array_filter($leads, fn($l) => $l['id'] !== $id));
- file_put_contents($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- addLog('lead_to_customer', $customer['id'], $customer['yritys'], 'Muutti liidin asiakkaaksi');
+ dbDeleteLead($id);
+ dbAddLog($companyId, currentUser(), 'lead_to_customer', $customer['id'], $customer['yritys'], 'Muutti liidin asiakkaaksi');
echo json_encode($customer);
break;
@@ -1940,13 +1447,13 @@ switch ($action) {
// ---------- TICKETS ----------
case 'tickets':
requireAuth();
- $allCompanies = !empty($_GET['all']);
+ $allCompaniesMode = !empty($_GET['all']);
$userCompanyIds = $_SESSION['companies'] ?? [];
// Kerää yritykset joista haetaan
$companiesToQuery = [];
- if ($allCompanies && count($userCompanyIds) > 1) {
- $allComps = loadCompanies();
+ if ($allCompaniesMode && count($userCompanyIds) > 1) {
+ $allComps = dbLoadCompanies();
foreach ($allComps as $c) {
if (in_array($c['id'], $userCompanyIds)) {
$companiesToQuery[] = $c;
@@ -1959,31 +1466,23 @@ switch ($action) {
$list = [];
foreach ($companiesToQuery as $comp) {
- $cDir = DATA_DIR . '/companies/' . $comp['id'];
- $ticketsFile = $cDir . '/tickets.json';
- if (!file_exists($ticketsFile)) continue;
- $tickets = json_decode(file_get_contents($ticketsFile), true) ?: [];
+ $tickets = dbLoadTickets($comp['id']);
// Auto-close tarkistus
$now = date('Y-m-d H:i:s');
- $autoCloseCount = 0;
foreach ($tickets as &$tc) {
if (!empty($tc['auto_close_at']) && $tc['auto_close_at'] <= $now && !in_array($tc['status'], ['suljettu'])) {
$tc['status'] = 'suljettu';
$tc['updated'] = $now;
- $autoCloseCount++;
+ dbSaveTicket($comp['id'], $tc);
}
}
unset($tc);
- if ($autoCloseCount > 0) {
- file_put_contents($ticketsFile, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- }
// Resolve mailbox names for this company
- $confFile = $cDir . '/config.json';
- $companyConf = file_exists($confFile) ? (json_decode(file_get_contents($confFile), true) ?: []) : [];
+ $mailboxes = dbLoadMailboxes($comp['id']);
$mailboxNames = [];
- foreach ($companyConf['mailboxes'] ?? [] as $mb) {
+ foreach ($mailboxes as $mb) {
$mailboxNames[$mb['id']] = $mb['nimi'];
}
@@ -2019,9 +1518,9 @@ switch ($action) {
case 'ticket_detail':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
$id = $_GET['id'] ?? '';
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$ticket = null;
foreach ($tickets as $t) {
if ($t['id'] === $id) { $ticket = $t; break; }
@@ -2036,10 +1535,10 @@ switch ($action) {
case 'ticket_fetch':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
- $companyConf = loadCompanyConfig();
+ $companyConf = dbGetCompanyConfig($companyId);
$mailboxes = array_filter($companyConf['mailboxes'] ?? [], fn($mb) => !empty($mb['aktiivinen']));
if (empty($mailboxes)) {
@@ -2048,7 +1547,7 @@ switch ($action) {
break;
}
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$newCount = 0;
$threadedCount = 0;
$errors = [];
@@ -2098,15 +1597,36 @@ switch ($action) {
'message_id' => $email['message_id'],
];
- $ticketIdx = findTicketByReferences($tickets, $email['in_reply_to'], $email['references']);
-
- if ($ticketIdx !== null) {
- $tickets[$ticketIdx]['messages'][] = $msg;
- $tickets[$ticketIdx]['updated'] = $email['date'];
- if (in_array($tickets[$ticketIdx]['status'], ['ratkaistu', 'suljettu'])) {
- $tickets[$ticketIdx]['status'] = 'kasittelyssa';
+ // Threading: find existing ticket by references
+ $existingTicket = null;
+ if ($email['in_reply_to']) {
+ $existingTicket = dbFindTicketByMessageId($companyId, $email['in_reply_to']);
+ }
+ if (!$existingTicket && $email['references']) {
+ $refs = preg_split('/\s+/', $email['references']);
+ foreach ($refs as $ref) {
+ $ref = trim($ref);
+ if (!$ref) continue;
+ $existingTicket = dbFindTicketByMessageId($companyId, $ref);
+ if ($existingTicket) break;
+ }
+ }
+
+ if ($existingTicket) {
+ // Load full ticket with messages
+ $fullTickets = dbLoadTickets($companyId);
+ foreach ($fullTickets as $ft) {
+ if ($ft['id'] === $existingTicket['id']) {
+ $ft['messages'][] = $msg;
+ $ft['updated'] = $email['date'];
+ if (in_array($ft['status'], ['ratkaistu', 'suljettu'])) {
+ $ft['status'] = 'kasittelyssa';
+ }
+ dbSaveTicket($companyId, $ft);
+ $threadedCount++;
+ break;
+ }
}
- $threadedCount++;
} else {
$ticket = [
'id' => generateId(),
@@ -2160,7 +1680,7 @@ switch ($action) {
}
}
- $tickets[] = $ticket;
+ dbSaveTicket($companyId, $ticket);
$newCount++;
}
@@ -2168,20 +1688,17 @@ switch ($action) {
}
}
- usort($tickets, function($a, $b) {
- return strcmp($b['updated'], $a['updated']);
- });
-
- saveTickets($tickets);
- addLog('ticket_fetch', '', '', "Haettu sähköpostit: {$newCount} uutta tikettiä, {$threadedCount} ketjutettu");
- $result = ['success' => true, 'new_tickets' => $newCount, 'threaded' => $threadedCount, 'total' => count($tickets)];
+ // Reload for total count
+ $allTickets = dbLoadTickets($companyId);
+ dbAddLog($companyId, currentUser(), 'ticket_fetch', '', '', "Haettu sähköpostit: {$newCount} uutta tikettiä, {$threadedCount} ketjutettu");
+ $result = ['success' => true, 'new_tickets' => $newCount, 'threaded' => $threadedCount, 'total' => count($allTickets)];
if (!empty($errors)) $result['errors'] = $errors;
echo json_encode($result);
break;
case 'ticket_reply':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
@@ -2191,9 +1708,9 @@ switch ($action) {
echo json_encode(['error' => 'Viesti ei voi olla tyhjä']);
break;
}
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$found = false;
- foreach ($tickets as &$t) {
+ foreach ($tickets as $t) {
if ($t['id'] === $id) {
// Find last message_id for threading
$lastMsgId = $t['message_id'] ?? '';
@@ -2206,7 +1723,7 @@ switch ($action) {
}
// Send email — hae postilaatikon asetukset
- $companyConf = loadCompanyConfig();
+ $companyConf = dbGetCompanyConfig($companyId);
$replyMailbox = null;
foreach ($companyConf['mailboxes'] ?? [] as $mb) {
if ($mb['id'] === ($t['mailbox_id'] ?? '')) { $replyMailbox = $mb; break; }
@@ -2219,12 +1736,9 @@ switch ($action) {
// Hae käyttäjän allekirjoitus tälle postilaatikolle
$mailboxId = $t['mailbox_id'] ?? '';
$signature = '';
- $usersForSig = loadUsers();
- foreach ($usersForSig as $sigUser) {
- if ($sigUser['id'] === $_SESSION['user_id']) {
- $signature = trim($sigUser['signatures'][$mailboxId] ?? '');
- break;
- }
+ $sigUser = dbGetUser($_SESSION['user_id']);
+ if ($sigUser) {
+ $signature = trim($sigUser['signatures'][$mailboxId] ?? '');
}
$emailBody = $signature ? $body . "\n\n-- \n" . $signature : $body;
@@ -2251,24 +1765,22 @@ switch ($action) {
$t['updated'] = date('Y-m-d H:i:s');
if ($t['status'] === 'uusi') $t['status'] = 'kasittelyssa';
+ dbSaveTicket($companyId, $t);
$found = true;
- addLog('ticket_reply', $t['id'], $t['subject'], 'Vastasi tikettiin');
+ dbAddLog($companyId, currentUser(), 'ticket_reply', $t['id'], $t['subject'], 'Vastasi tikettiin');
echo json_encode($t);
break;
}
}
- unset($t);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
- break;
}
- saveTickets($tickets);
break;
case 'ticket_status':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
@@ -2279,31 +1791,29 @@ switch ($action) {
echo json_encode(['error' => 'Virheellinen tila']);
break;
}
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$found = false;
- foreach ($tickets as &$t) {
+ foreach ($tickets as $t) {
if ($t['id'] === $id) {
$oldStatus = $t['status'];
$t['status'] = $status;
$t['updated'] = date('Y-m-d H:i:s');
+ dbSaveTicket($companyId, $t);
$found = true;
- addLog('ticket_status', $t['id'], $t['subject'], "Tila: {$oldStatus} → {$status}");
+ dbAddLog($companyId, currentUser(), 'ticket_status', $t['id'], $t['subject'], "Tila: {$oldStatus} → {$status}");
echo json_encode($t);
break;
}
}
- unset($t);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
- break;
}
- saveTickets($tickets);
break;
case 'ticket_type':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
@@ -2314,89 +1824,83 @@ switch ($action) {
echo json_encode(['error' => 'Virheellinen tyyppi']);
break;
}
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$found = false;
- foreach ($tickets as &$t) {
+ foreach ($tickets as $t) {
if ($t['id'] === $id) {
$oldType = $t['type'] ?? 'muu';
$t['type'] = $type;
$t['updated'] = date('Y-m-d H:i:s');
+ dbSaveTicket($companyId, $t);
$found = true;
- addLog('ticket_type', $t['id'], $t['subject'], "Tyyppi: {$oldType} → {$type}");
+ dbAddLog($companyId, currentUser(), 'ticket_type', $t['id'], $t['subject'], "Tyyppi: {$oldType} → {$type}");
echo json_encode($t);
break;
}
}
- unset($t);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
- break;
}
- saveTickets($tickets);
break;
case 'ticket_customer':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$customerId = $input['customer_id'] ?? '';
$customerName = $input['customer_name'] ?? '';
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$found = false;
- foreach ($tickets as &$t) {
+ foreach ($tickets as $t) {
if ($t['id'] === $id) {
$t['customer_id'] = $customerId;
$t['customer_name'] = $customerName;
$t['updated'] = date('Y-m-d H:i:s');
+ dbSaveTicket($companyId, $t);
$found = true;
- addLog('ticket_customer', $t['id'], $t['subject'], "Asiakkuus: {$customerName}");
+ dbAddLog($companyId, currentUser(), 'ticket_customer', $t['id'], $t['subject'], "Asiakkuus: {$customerName}");
echo json_encode($t);
break;
}
}
- unset($t);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
- break;
}
- saveTickets($tickets);
break;
case 'ticket_assign':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$assignTo = trim($input['assigned_to'] ?? '');
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$found = false;
- foreach ($tickets as &$t) {
+ foreach ($tickets as $t) {
if ($t['id'] === $id) {
$t['assigned_to'] = $assignTo;
$t['updated'] = date('Y-m-d H:i:s');
+ dbSaveTicket($companyId, $t);
$found = true;
- addLog('ticket_assign', $t['id'], $t['subject'], "Osoitettu: {$assignTo}");
+ dbAddLog($companyId, currentUser(), 'ticket_assign', $t['id'], $t['subject'], "Osoitettu: {$assignTo}");
echo json_encode($t);
break;
}
}
- unset($t);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
- break;
}
- saveTickets($tickets);
break;
case 'ticket_note':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
@@ -2406,9 +1910,9 @@ switch ($action) {
echo json_encode(['error' => 'Muistiinpano ei voi olla tyhjä']);
break;
}
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$found = false;
- foreach ($tickets as &$t) {
+ foreach ($tickets as $t) {
if ($t['id'] === $id) {
$note = [
'id' => generateId(),
@@ -2421,41 +1925,38 @@ switch ($action) {
];
$t['messages'][] = $note;
$t['updated'] = date('Y-m-d H:i:s');
+ dbSaveTicket($companyId, $t);
$found = true;
- addLog('ticket_note', $t['id'], $t['subject'], 'Lisäsi muistiinpanon');
+ dbAddLog($companyId, currentUser(), 'ticket_note', $t['id'], $t['subject'], 'Lisäsi muistiinpanon');
echo json_encode($t);
break;
}
}
- unset($t);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
- break;
}
- saveTickets($tickets);
break;
case 'ticket_delete':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$deleted = null;
foreach ($tickets as $t) {
if ($t['id'] === $id) { $deleted = $t; break; }
}
- $tickets = array_values(array_filter($tickets, fn($t) => $t['id'] !== $id));
- saveTickets($tickets);
- if ($deleted) addLog('ticket_delete', $id, $deleted['subject'] ?? '', 'Poisti tiketin');
+ dbDeleteTicket($id);
+ if ($deleted) dbAddLog($companyId, currentUser(), 'ticket_delete', $id, $deleted['subject'] ?? '', 'Poisti tiketin');
echo json_encode(['success' => true]);
break;
case 'ticket_tags':
requireAuth();
- requireCompanyOrParam();
+ $companyId = requireCompanyOrParam();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
@@ -2464,41 +1965,36 @@ switch ($action) {
$tags = array_values(array_filter(array_map(function($t) {
return trim(strtolower($t));
}, $tags)));
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$found = false;
- foreach ($tickets as &$t) {
+ foreach ($tickets as $t) {
if ($t['id'] === $id) {
$t['tags'] = $tags;
$t['updated'] = date('Y-m-d H:i:s');
+ dbSaveTicket($companyId, $t);
$found = true;
- addLog('ticket_tags', $t['id'], $t['subject'], 'Tagit: ' . implode(', ', $tags));
+ dbAddLog($companyId, currentUser(), 'ticket_tags', $t['id'], $t['subject'], 'Tagit: ' . implode(', ', $tags));
echo json_encode($t);
break;
}
}
- unset($t);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
- break;
}
- saveTickets($tickets);
break;
case 'ticket_rules':
requireAuth();
- requireCompany();
- $companyConf = loadCompanyConfig();
- echo json_encode($companyConf['ticket_rules'] ?? []);
+ $companyId = requireCompany();
+ echo json_encode(dbLoadTicketRules($companyId));
break;
case 'ticket_rule_save':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
- $companyConf = loadCompanyConfig();
- $rules = $companyConf['ticket_rules'] ?? [];
$rule = [
'id' => $input['id'] ?? generateId(),
@@ -2518,26 +2014,14 @@ switch ($action) {
break;
}
- $found = false;
- foreach ($rules as &$r) {
- if ($r['id'] === $rule['id']) {
- $r = $rule;
- $found = true;
- break;
- }
- }
- unset($r);
- if (!$found) $rules[] = $rule;
-
- $companyConf['ticket_rules'] = $rules;
- saveCompanyConfig($companyConf);
- addLog('config_update', '', '', 'Tikettisääntö: ' . $rule['name']);
+ dbSaveTicketRule($companyId, $rule);
+ dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Tikettisääntö: ' . $rule['name']);
echo json_encode($rule);
break;
case 'ticket_bulk_status':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$ids = $input['ids'] ?? [];
@@ -2548,46 +2032,42 @@ switch ($action) {
echo json_encode(['error' => 'Virheellinen tila']);
break;
}
- $tickets = loadTickets();
+ $tickets = dbLoadTickets($companyId);
$changed = 0;
- foreach ($tickets as &$t) {
+ foreach ($tickets as $t) {
if (in_array($t['id'], $ids)) {
$t['status'] = $newStatus;
$t['updated'] = date('Y-m-d H:i:s');
+ dbSaveTicket($companyId, $t);
$changed++;
}
}
- unset($t);
- saveTickets($tickets);
- addLog('ticket_status', '', '', "Massapäivitys: $changed tikettiä → $newStatus");
+ dbAddLog($companyId, currentUser(), 'ticket_status', '', '', "Massapäivitys: $changed tikettiä → $newStatus");
echo json_encode(['success' => true, 'changed' => $changed]);
break;
case 'ticket_bulk_delete':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$ids = $input['ids'] ?? [];
- $tickets = loadTickets();
- $before = count($tickets);
- $tickets = array_values(array_filter($tickets, fn($t) => !in_array($t['id'], $ids)));
- $deleted = $before - count($tickets);
- saveTickets($tickets);
- addLog('ticket_delete', '', '', "Massapoisto: $deleted tikettiä");
+ $deleted = 0;
+ foreach ($ids as $ticketId) {
+ dbDeleteTicket($ticketId);
+ $deleted++;
+ }
+ dbAddLog($companyId, currentUser(), 'ticket_delete', '', '', "Massapoisto: $deleted tikettiä");
echo json_encode(['success' => true, 'deleted' => $deleted]);
break;
case 'ticket_rule_delete':
requireAuth();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$ruleId = $input['id'] ?? '';
- $companyConf = loadCompanyConfig();
- $rules = $companyConf['ticket_rules'] ?? [];
- $companyConf['ticket_rules'] = array_values(array_filter($rules, fn($r) => $r['id'] !== $ruleId));
- saveCompanyConfig($companyConf);
+ dbDeleteTicketRule($ruleId);
echo json_encode(['success' => true]);
break;
@@ -2595,29 +2075,26 @@ switch ($action) {
case 'companies':
requireAuth();
$userCompanyIds = $_SESSION['companies'] ?? [];
- $allCompanies = loadCompanies();
+ $allCompanies = dbLoadCompanies();
$result = array_values(array_filter($allCompanies, fn($c) => in_array($c['id'], $userCompanyIds)));
echo json_encode($result);
break;
case 'companies_all':
requireAdmin();
- echo json_encode(loadCompanies());
+ echo json_encode(dbLoadCompanies());
break;
case 'all_mailboxes':
requireAuth();
// Palauttaa kaikki postilaatikot käyttäjän yrityksistä (allekirjoituksia varten)
$userCompanyIds = $_SESSION['companies'] ?? [];
- $allCompanies = loadCompanies();
+ $allCompanies = dbLoadCompanies();
$result = [];
foreach ($allCompanies as $comp) {
if (!in_array($comp['id'], $userCompanyIds)) continue;
- $oldCompanyId = $_SESSION['company_id'] ?? '';
- $_SESSION['company_id'] = $comp['id'];
- $conf = loadCompanyConfig();
- $_SESSION['company_id'] = $oldCompanyId;
- foreach ($conf['mailboxes'] ?? [] as $mb) {
+ $mailboxes = dbLoadMailboxes($comp['id']);
+ foreach ($mailboxes as $mb) {
$result[] = [
'id' => $mb['id'],
'nimi' => $mb['nimi'] ?? $mb['imap_user'] ?? '',
@@ -2640,7 +2117,7 @@ switch ($action) {
echo json_encode(['error' => 'ID ja nimi vaaditaan']);
break;
}
- $companies = loadCompanies();
+ $companies = dbLoadCompanies();
foreach ($companies as $c) {
if ($c['id'] === $id) {
http_response_code(400);
@@ -2663,28 +2140,17 @@ switch ($action) {
'luotu' => date('Y-m-d H:i:s'),
'aktiivinen' => true,
];
- $companies[] = $company;
- saveCompanies($companies);
- // Luo hakemisto ja tyhjät tiedostot
+ dbSaveCompany($company);
+ // Luo hakemisto (tiedostoja varten)
$compDir = DATA_DIR . '/companies/' . $id;
if (!file_exists($compDir)) mkdir($compDir, 0755, true);
- file_put_contents($compDir . '/config.json', json_encode(['mailboxes' => [], 'ticket_rules' => []], JSON_PRETTY_PRINT));
- file_put_contents($compDir . '/customers.json', '[]');
- file_put_contents($compDir . '/leads.json', '[]');
- file_put_contents($compDir . '/archive.json', '[]');
- file_put_contents($compDir . '/tickets.json', '[]');
- file_put_contents($compDir . '/changelog.json', '[]');
// Lisää luoja yrityksen käyttäjäksi
- $users = loadUsers();
- foreach ($users as &$u) {
- if ($u['id'] === $_SESSION['user_id']) {
- $u['companies'] = array_unique(array_merge($u['companies'] ?? [], [$id]));
- $_SESSION['companies'] = $u['companies'];
- break;
- }
+ $u = dbGetUser($_SESSION['user_id']);
+ if ($u) {
+ $u['companies'] = array_unique(array_merge($u['companies'] ?? [], [$id]));
+ dbSaveUser($u);
+ $_SESSION['companies'] = $u['companies'];
}
- unset($u);
- saveUsers($users);
echo json_encode($company);
break;
@@ -2693,9 +2159,9 @@ switch ($action) {
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
- $companies = loadCompanies();
+ $companies = dbLoadCompanies();
$found = false;
- foreach ($companies as &$c) {
+ foreach ($companies as $c) {
if ($c['id'] === $id) {
if (isset($input['nimi'])) $c['nimi'] = trim($input['nimi']);
if (isset($input['aktiivinen'])) $c['aktiivinen'] = (bool)$input['aktiivinen'];
@@ -2704,18 +2170,16 @@ switch ($action) {
}
if (isset($input['primary_color'])) $c['primary_color'] = trim($input['primary_color']);
if (isset($input['subtitle'])) $c['subtitle'] = trim($input['subtitle']);
+ dbSaveCompany($c);
$found = true;
echo json_encode($c);
break;
}
}
- unset($c);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Yritystä ei löydy']);
- break;
}
- saveCompanies($companies);
break;
case 'company_delete':
@@ -2728,16 +2192,13 @@ switch ($action) {
echo json_encode(['error' => 'Virheellinen yritys-ID']);
break;
}
- $companies = loadCompanies();
- $companies = array_values(array_filter($companies, fn($c) => $c['id'] !== $id));
- saveCompanies($companies);
- // Poista yritys käyttäjiltä
- $users = loadUsers();
- foreach ($users as &$u) {
+ dbDeleteCompany($id);
+ // Poista yritys käyttäjiltä (CASCADE hoitaa tietokannan puolella, mutta päivitetään sessio)
+ $users = dbLoadUsers();
+ foreach ($users as $u) {
$u['companies'] = array_values(array_filter($u['companies'] ?? [], fn($c) => $c !== $id));
+ dbSaveUser($u);
}
- unset($u);
- saveUsers($users);
echo json_encode(['success' => true]);
break;
@@ -2758,42 +2219,58 @@ switch ($action) {
case 'company_config':
requireAdmin();
- requireCompany();
- echo json_encode(loadCompanyConfig());
+ $companyId = requireCompany();
+ echo json_encode(dbGetCompanyConfig($companyId));
break;
case 'company_config_update':
requireAdmin();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
- $companyConf = loadCompanyConfig();
- if (isset($input['mailboxes'])) $companyConf['mailboxes'] = $input['mailboxes'];
- if (isset($input['ticket_rules'])) $companyConf['ticket_rules'] = $input['ticket_rules'];
- saveCompanyConfig($companyConf);
- echo json_encode($companyConf);
+ if (isset($input['mailboxes'])) {
+ // Delete all existing mailboxes and re-save
+ $existingMailboxes = dbLoadMailboxes($companyId);
+ foreach ($existingMailboxes as $existing) {
+ dbDeleteMailbox($existing['id']);
+ }
+ foreach ($input['mailboxes'] as $mb) {
+ if (empty($mb['id'])) $mb['id'] = generateId();
+ dbSaveMailbox($companyId, $mb);
+ }
+ }
+ if (isset($input['ticket_rules'])) {
+ // Delete all existing rules and re-save
+ $existingRules = dbLoadTicketRules($companyId);
+ foreach ($existingRules as $existing) {
+ dbDeleteTicketRule($existing['id']);
+ }
+ foreach ($input['ticket_rules'] as $rule) {
+ if (empty($rule['id'])) $rule['id'] = generateId();
+ dbSaveTicketRule($companyId, $rule);
+ }
+ }
+ echo json_encode(dbGetCompanyConfig($companyId));
break;
// ---------- MAILBOXES ----------
case 'mailboxes':
requireAuth();
- requireCompany();
- $companyConf = loadCompanyConfig();
+ $companyId = requireCompany();
+ $mailboxes = dbLoadMailboxes($companyId);
// Palauta postilaatikot ilman salasanoja
$mbs = array_map(function($mb) {
$mb['imap_password'] = !empty($mb['imap_password']) ? '********' : '';
return $mb;
- }, $companyConf['mailboxes'] ?? []);
+ }, $mailboxes);
echo json_encode($mbs);
break;
case 'mailbox_save':
requireAdmin();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
- $companyConf = loadCompanyConfig();
- $mailboxes = $companyConf['mailboxes'] ?? [];
$mb = [
'id' => $input['id'] ?? generateId(),
@@ -2806,18 +2283,13 @@ switch ($action) {
'smtp_from_name' => trim($input['smtp_from_name'] ?? ''),
'aktiivinen' => $input['aktiivinen'] ?? true,
];
- // Salasana: jos ******** → pidä vanha, muuten päivitä
+ // Salasana: jos ******** -> pidä vanha, muuten päivitä
if (isset($input['imap_password']) && $input['imap_password'] !== '********') {
$mb['imap_password'] = $input['imap_password'];
} else {
// Hae vanha salasana
- foreach ($mailboxes as $existing) {
- if ($existing['id'] === $mb['id']) {
- $mb['imap_password'] = $existing['imap_password'] ?? '';
- break;
- }
- }
- if (!isset($mb['imap_password'])) $mb['imap_password'] = '';
+ $existingMb = dbGetMailbox($mb['id']);
+ $mb['imap_password'] = $existingMb ? ($existingMb['imap_password'] ?? '') : '';
}
if (empty($mb['nimi'])) {
@@ -2826,20 +2298,8 @@ switch ($action) {
break;
}
- $found = false;
- foreach ($mailboxes as &$existing) {
- if ($existing['id'] === $mb['id']) {
- $existing = $mb;
- $found = true;
- break;
- }
- }
- unset($existing);
- if (!$found) $mailboxes[] = $mb;
-
- $companyConf['mailboxes'] = $mailboxes;
- saveCompanyConfig($companyConf);
- addLog('mailbox_save', '', '', 'Postilaatikko: ' . $mb['nimi']);
+ dbSaveMailbox($companyId, $mb);
+ dbAddLog($companyId, currentUser(), 'mailbox_save', '', '', 'Postilaatikko: ' . $mb['nimi']);
// Palauta ilman salasanaa
$mb['imap_password'] = '********';
echo json_encode($mb);
@@ -2847,13 +2307,11 @@ switch ($action) {
case 'mailbox_delete':
requireAdmin();
- requireCompany();
+ $companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$mbId = $input['id'] ?? '';
- $companyConf = loadCompanyConfig();
- $companyConf['mailboxes'] = array_values(array_filter($companyConf['mailboxes'] ?? [], fn($m) => $m['id'] !== $mbId));
- saveCompanyConfig($companyConf);
+ dbDeleteMailbox($mbId);
echo json_encode(['success' => true]);
break;
diff --git a/db.php b/db.php
new file mode 100644
index 0000000..248cab1
--- /dev/null
+++ b/db.php
@@ -0,0 +1,1046 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ]);
+ }
+ return $pdo;
+}
+
+// ==================== TAULUJEN LUONTI ====================
+
+function initDatabase(): void {
+ $db = getDb();
+ $db->exec("
+ CREATE TABLE IF NOT EXISTS companies (
+ id VARCHAR(50) PRIMARY KEY,
+ nimi VARCHAR(255) NOT NULL,
+ luotu DATETIME,
+ aktiivinen BOOLEAN DEFAULT TRUE,
+ primary_color VARCHAR(7) DEFAULT '#0f3460',
+ subtitle VARCHAR(255) DEFAULT '',
+ logo_file VARCHAR(255) DEFAULT '',
+ api_key VARCHAR(64) DEFAULT '',
+ cors_origins TEXT DEFAULT ''
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS company_domains (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ domain VARCHAR(255) NOT NULL,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ UNIQUE KEY udx_domain (domain)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS users (
+ id VARCHAR(20) PRIMARY KEY,
+ username VARCHAR(100) NOT NULL UNIQUE,
+ password_hash VARCHAR(255) NOT NULL,
+ nimi VARCHAR(255) NOT NULL,
+ role ENUM('admin','user') DEFAULT 'user',
+ email VARCHAR(255) DEFAULT '',
+ luotu DATETIME
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS user_companies (
+ user_id VARCHAR(20) NOT NULL,
+ company_id VARCHAR(50) NOT NULL,
+ PRIMARY KEY (user_id, company_id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB;
+
+ CREATE TABLE IF NOT EXISTS user_signatures (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id VARCHAR(20) NOT NULL,
+ mailbox_id VARCHAR(20) NOT NULL,
+ signature TEXT,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE KEY udx_user_mailbox (user_id, mailbox_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS config (
+ config_key VARCHAR(100) PRIMARY KEY,
+ config_value TEXT
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS reset_tokens (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id VARCHAR(20) NOT NULL,
+ token VARCHAR(64) NOT NULL UNIQUE,
+ created_at DATETIME NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB;
+
+ CREATE TABLE IF NOT EXISTS login_attempts (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ ip VARCHAR(45) NOT NULL,
+ attempted_at DATETIME NOT NULL,
+ INDEX idx_ip_time (ip, attempted_at)
+ ) ENGINE=InnoDB;
+
+ CREATE TABLE IF NOT EXISTS customers (
+ id VARCHAR(20) PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ yritys VARCHAR(255),
+ yhteyshenkilö VARCHAR(255),
+ puhelin VARCHAR(100),
+ sahkoposti VARCHAR(255),
+ laskutusosoite TEXT,
+ laskutuspostinumero VARCHAR(20),
+ laskutuskaupunki VARCHAR(100),
+ laskutussahkoposti VARCHAR(255),
+ elaskuosoite VARCHAR(100),
+ elaskuvalittaja VARCHAR(100),
+ ytunnus VARCHAR(20),
+ lisatiedot TEXT,
+ luotu DATETIME,
+ muokattu DATETIME NULL,
+ muokkaaja VARCHAR(100) DEFAULT '',
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ INDEX idx_company (company_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS customer_connections (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ customer_id VARCHAR(20) NOT NULL,
+ asennusosoite VARCHAR(255) DEFAULT '',
+ postinumero VARCHAR(20) DEFAULT '',
+ kaupunki VARCHAR(100) DEFAULT '',
+ liittymanopeus VARCHAR(50) DEFAULT '',
+ hinta DECIMAL(10,2) DEFAULT 0,
+ sopimuskausi VARCHAR(100) DEFAULT '',
+ alkupvm VARCHAR(20) DEFAULT '',
+ FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS leads (
+ id VARCHAR(20) PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ yritys VARCHAR(255),
+ yhteyshenkilo VARCHAR(255),
+ puhelin VARCHAR(100),
+ sahkoposti VARCHAR(255),
+ osoite TEXT,
+ kaupunki VARCHAR(100),
+ tila VARCHAR(50) DEFAULT 'uusi',
+ muistiinpanot TEXT,
+ luotu DATETIME,
+ luoja VARCHAR(100),
+ muokattu DATETIME NULL,
+ muokkaaja VARCHAR(100) DEFAULT '',
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ INDEX idx_company (company_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS tickets (
+ id VARCHAR(20) PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ subject VARCHAR(500),
+ from_email VARCHAR(255),
+ from_name VARCHAR(255),
+ status VARCHAR(20) DEFAULT 'uusi',
+ type VARCHAR(20) DEFAULT 'muu',
+ assigned_to VARCHAR(100) DEFAULT '',
+ customer_id VARCHAR(20) DEFAULT '',
+ customer_name VARCHAR(255) DEFAULT '',
+ message_id VARCHAR(500) DEFAULT '',
+ mailbox_id VARCHAR(20) DEFAULT '',
+ auto_close_at VARCHAR(30) DEFAULT '',
+ created DATETIME,
+ updated DATETIME,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ INDEX idx_company (company_id),
+ INDEX idx_status (status),
+ INDEX idx_message_id (message_id(255))
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS ticket_messages (
+ id VARCHAR(20) PRIMARY KEY,
+ ticket_id VARCHAR(20) NOT NULL,
+ type VARCHAR(20) NOT NULL,
+ from_email VARCHAR(255) DEFAULT '',
+ from_name VARCHAR(255) DEFAULT '',
+ body LONGTEXT,
+ timestamp DATETIME,
+ message_id VARCHAR(500) DEFAULT '',
+ FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
+ INDEX idx_ticket (ticket_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS ticket_tags (
+ ticket_id VARCHAR(20) NOT NULL,
+ tag VARCHAR(100) NOT NULL,
+ PRIMARY KEY (ticket_id, tag),
+ FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS archives (
+ id VARCHAR(20) PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ data JSON NOT NULL,
+ archived_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ INDEX idx_company (company_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS changelog (
+ id VARCHAR(20) PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ timestamp DATETIME NOT NULL,
+ user VARCHAR(100),
+ action VARCHAR(100),
+ customer_id VARCHAR(20) DEFAULT '',
+ customer_name VARCHAR(255) DEFAULT '',
+ details TEXT,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ INDEX idx_company_time (company_id, timestamp)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS mailboxes (
+ id VARCHAR(20) PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ nimi VARCHAR(255),
+ imap_host VARCHAR(255),
+ imap_port INT DEFAULT 993,
+ imap_user VARCHAR(255),
+ imap_encryption VARCHAR(10) DEFAULT 'ssl',
+ imap_password VARCHAR(255),
+ smtp_from_email VARCHAR(255),
+ smtp_from_name VARCHAR(255),
+ aktiivinen BOOLEAN DEFAULT TRUE,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ INDEX idx_company (company_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS ticket_rules (
+ id VARCHAR(20) PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ name VARCHAR(255),
+ from_contains VARCHAR(255),
+ priority INT DEFAULT 0,
+ tag VARCHAR(100) DEFAULT '',
+ assign_to VARCHAR(100) DEFAULT '',
+ status_set VARCHAR(20) DEFAULT '',
+ type_set VARCHAR(20) DEFAULT '',
+ auto_close_days INT DEFAULT 0,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ INDEX idx_company (company_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ CREATE TABLE IF NOT EXISTS files (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ company_id VARCHAR(50) NOT NULL,
+ customer_id VARCHAR(20) NOT NULL,
+ filename VARCHAR(255) NOT NULL,
+ original_name VARCHAR(255),
+ size INT DEFAULT 0,
+ uploaded_at DATETIME,
+ uploaded_by VARCHAR(100),
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ INDEX idx_company_customer (company_id, customer_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+ ");
+}
+
+// ==================== YRITYKSET ====================
+
+function dbLoadCompanies(): array {
+ $db = getDb();
+ $companies = $db->query("SELECT * FROM companies ORDER BY nimi")->fetchAll();
+
+ // Liitä domainit
+ foreach ($companies as &$c) {
+ $stmt = $db->prepare("SELECT domain FROM company_domains WHERE company_id = ?");
+ $stmt->execute([$c['id']]);
+ $c['domains'] = $stmt->fetchAll(PDO::FETCH_COLUMN);
+ $c['aktiivinen'] = (bool)$c['aktiivinen'];
+ }
+ return $companies;
+}
+
+function dbSaveCompany(array $company): void {
+ $db = getDb();
+ $db->beginTransaction();
+ try {
+ $stmt = $db->prepare("
+ 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)
+ 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)
+ ");
+ $stmt->execute([
+ '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'] ?? '',
+ ]);
+
+ // Päivitä domainit
+ $db->prepare("DELETE FROM company_domains WHERE company_id = ?")->execute([$company['id']]);
+ if (!empty($company['domains'])) {
+ $ins = $db->prepare("INSERT INTO company_domains (company_id, domain) VALUES (?, ?)");
+ foreach ($company['domains'] as $domain) {
+ $domain = trim($domain);
+ if ($domain) $ins->execute([$company['id'], $domain]);
+ }
+ }
+ $db->commit();
+ } catch (Exception $e) {
+ $db->rollBack();
+ throw $e;
+ }
+}
+
+function dbDeleteCompany(string $companyId): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM companies WHERE id = ?")->execute([$companyId]);
+}
+
+function dbGetBranding(string $host): array {
+ $db = getDb();
+ $host = strtolower(trim($host));
+
+ $stmt = $db->prepare("
+ SELECT c.* FROM companies c
+ JOIN company_domains cd ON c.id = cd.company_id
+ WHERE LOWER(cd.domain) = ?
+ LIMIT 1
+ ");
+ $stmt->execute([$host]);
+ $company = $stmt->fetch();
+
+ if ($company) {
+ $logoUrl = !empty($company['logo_file'])
+ ? "api.php?action=company_logo&company_id=" . urlencode($company['id'])
+ : '';
+ return [
+ 'found' => true,
+ 'company_id' => $company['id'],
+ 'nimi' => $company['nimi'],
+ 'primary_color' => $company['primary_color'] ?? '#0f3460',
+ 'subtitle' => $company['subtitle'] ?? '',
+ 'logo_url' => $logoUrl,
+ ];
+ }
+
+ return [
+ 'found' => false,
+ 'company_id' => '',
+ 'nimi' => 'Noxus Intra',
+ 'primary_color' => '#0f3460',
+ 'subtitle' => 'Hallintapaneeli',
+ 'logo_url' => '',
+ ];
+}
+
+function dbGetCompanyByDomain(string $host): ?array {
+ $db = getDb();
+ $stmt = $db->prepare("
+ SELECT c.* FROM companies c
+ JOIN company_domains cd ON c.id = cd.company_id
+ WHERE LOWER(cd.domain) = ?
+ LIMIT 1
+ ");
+ $stmt->execute([strtolower(trim($host))]);
+ return $stmt->fetch() ?: null;
+}
+
+function dbGetCompanyByApiKey(string $apiKey): ?array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM companies WHERE api_key = ? AND api_key != '' LIMIT 1");
+ $stmt->execute([$apiKey]);
+ return $stmt->fetch() ?: null;
+}
+
+// ==================== KÄYTTÄJÄT ====================
+
+function dbLoadUsers(): array {
+ $db = getDb();
+ $users = $db->query("SELECT * FROM users ORDER BY luotu")->fetchAll();
+
+ foreach ($users as &$u) {
+ // Yritykset
+ $stmt = $db->prepare("SELECT company_id FROM user_companies WHERE user_id = ?");
+ $stmt->execute([$u['id']]);
+ $u['companies'] = $stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ // Allekirjoitukset
+ $stmt = $db->prepare("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?");
+ $stmt->execute([$u['id']]);
+ $sigs = [];
+ foreach ($stmt->fetchAll() as $row) {
+ $sigs[$row['mailbox_id']] = $row['signature'];
+ }
+ $u['signatures'] = $sigs;
+ }
+ return $users;
+}
+
+function dbGetUser(string $id): ?array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
+ $stmt->execute([$id]);
+ $u = $stmt->fetch();
+ if (!$u) return null;
+
+ $stmt = $db->prepare("SELECT company_id FROM user_companies WHERE user_id = ?");
+ $stmt->execute([$id]);
+ $u['companies'] = $stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ $stmt = $db->prepare("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?");
+ $stmt->execute([$id]);
+ $sigs = [];
+ foreach ($stmt->fetchAll() as $row) {
+ $sigs[$row['mailbox_id']] = $row['signature'];
+ }
+ $u['signatures'] = $sigs;
+
+ return $u;
+}
+
+function dbGetUserByUsername(string $username): ?array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM users WHERE username = ?");
+ $stmt->execute([$username]);
+ $u = $stmt->fetch();
+ if (!$u) return null;
+
+ $stmt = $db->prepare("SELECT company_id FROM user_companies WHERE user_id = ?");
+ $stmt->execute([$u['id']]);
+ $u['companies'] = $stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ $stmt = $db->prepare("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?");
+ $stmt->execute([$u['id']]);
+ $sigs = [];
+ foreach ($stmt->fetchAll() as $row) {
+ $sigs[$row['mailbox_id']] = $row['signature'];
+ }
+ $u['signatures'] = $sigs;
+
+ return $u;
+}
+
+function dbSaveUser(array $user): void {
+ $db = getDb();
+ $db->beginTransaction();
+ try {
+ $stmt = $db->prepare("
+ INSERT INTO users (id, username, password_hash, nimi, role, email, luotu)
+ VALUES (:id, :username, :password_hash, :nimi, :role, :email, :luotu)
+ ON DUPLICATE KEY UPDATE
+ username = VALUES(username), password_hash = VALUES(password_hash),
+ nimi = VALUES(nimi), role = VALUES(role), email = VALUES(email)
+ ");
+ $stmt->execute([
+ 'id' => $user['id'],
+ 'username' => $user['username'],
+ 'password_hash' => $user['password_hash'],
+ 'nimi' => $user['nimi'],
+ 'role' => $user['role'] ?? 'user',
+ 'email' => $user['email'] ?? '',
+ 'luotu' => $user['luotu'] ?? date('Y-m-d H:i:s'),
+ ]);
+
+ // Yritykset
+ $db->prepare("DELETE FROM user_companies WHERE user_id = ?")->execute([$user['id']]);
+ if (!empty($user['companies'])) {
+ $ins = $db->prepare("INSERT IGNORE INTO user_companies (user_id, company_id) VALUES (?, ?)");
+ foreach ($user['companies'] as $cid) {
+ $ins->execute([$user['id'], $cid]);
+ }
+ }
+
+ // Allekirjoitukset
+ $db->prepare("DELETE FROM user_signatures WHERE user_id = ?")->execute([$user['id']]);
+ if (!empty($user['signatures'])) {
+ $ins = $db->prepare("INSERT INTO user_signatures (user_id, mailbox_id, signature) VALUES (?, ?, ?)");
+ foreach ($user['signatures'] as $mbId => $sig) {
+ $ins->execute([$user['id'], $mbId, $sig]);
+ }
+ }
+
+ $db->commit();
+ } catch (Exception $e) {
+ $db->rollBack();
+ throw $e;
+ }
+}
+
+function dbDeleteUser(string $userId): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM users WHERE id = ?")->execute([$userId]);
+}
+
+// ==================== ASETUKSET (global) ====================
+
+function dbLoadConfig(): array {
+ $db = getDb();
+ $rows = $db->query("SELECT config_key, config_value FROM config")->fetchAll();
+ $config = [];
+ foreach ($rows as $row) {
+ $decoded = json_decode($row['config_value'], true);
+ $config[$row['config_key']] = $decoded !== null ? $decoded : $row['config_value'];
+ }
+ return $config;
+}
+
+function dbSaveConfig(array $config): void {
+ $db = getDb();
+ $stmt = $db->prepare("
+ INSERT INTO config (config_key, config_value) VALUES (?, ?)
+ ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)
+ ");
+ foreach ($config as $key => $value) {
+ $stmt->execute([$key, is_array($value) ? json_encode($value) : $value]);
+ }
+}
+
+// ==================== SALASANAN PALAUTUS ====================
+
+function dbSaveToken(string $userId, string $token): void {
+ $db = getDb();
+ $stmt = $db->prepare("INSERT INTO reset_tokens (user_id, token, created_at) VALUES (?, ?, NOW())");
+ $stmt->execute([$userId, hash('sha256', $token)]);
+}
+
+function dbValidateToken(string $token): ?string {
+ $db = getDb();
+ $hash = hash('sha256', $token);
+ $stmt = $db->prepare("SELECT user_id FROM reset_tokens WHERE token = ? AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)");
+ $stmt->execute([$hash]);
+ $row = $stmt->fetch();
+ return $row ? $row['user_id'] : null;
+}
+
+function dbRemoveToken(string $token): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM reset_tokens WHERE token = ?")->execute([hash('sha256', $token)]);
+ // Siivoa vanhentuneet
+ $db->exec("DELETE FROM reset_tokens WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 HOUR)");
+}
+
+// ==================== RATE LIMITING ====================
+
+function dbCheckRateLimit(string $ip): bool {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT COUNT(*) FROM login_attempts WHERE ip = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)");
+ $stmt->execute([$ip]);
+ return $stmt->fetchColumn() < 10;
+}
+
+function dbRecordLoginAttempt(string $ip): void {
+ $db = getDb();
+ $db->prepare("INSERT INTO login_attempts (ip, attempted_at) VALUES (?, NOW())")->execute([$ip]);
+ // Siivoa vanhat (yli 1h)
+ $db->exec("DELETE FROM login_attempts WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 1 HOUR)");
+}
+
+// ==================== ASIAKKAAT ====================
+
+function dbLoadCustomers(string $companyId): array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM customers WHERE company_id = ? ORDER BY yritys");
+ $stmt->execute([$companyId]);
+ $customers = $stmt->fetchAll();
+
+ // Liitä liittymät
+ $connStmt = $db->prepare("SELECT * FROM customer_connections WHERE customer_id = ?");
+ foreach ($customers as &$c) {
+ $connStmt->execute([$c['id']]);
+ $conns = $connStmt->fetchAll();
+ $c['liittymat'] = array_map(function($conn) {
+ return [
+ 'asennusosoite' => $conn['asennusosoite'] ?? '',
+ 'postinumero' => $conn['postinumero'] ?? '',
+ 'kaupunki' => $conn['kaupunki'] ?? '',
+ 'liittymanopeus' => $conn['liittymanopeus'] ?? '',
+ 'hinta' => (float)($conn['hinta'] ?? 0),
+ 'sopimuskausi' => $conn['sopimuskausi'] ?? '',
+ 'alkupvm' => $conn['alkupvm'] ?? '',
+ ];
+ }, $conns);
+ // Poista company_id vastauksesta (ei tarvita frontissa)
+ unset($c['company_id']);
+ }
+ return $customers;
+}
+
+function dbSaveCustomer(string $companyId, array $customer): void {
+ $db = getDb();
+ $db->beginTransaction();
+ try {
+ $stmt = $db->prepare("
+ INSERT INTO customers (id, company_id, yritys, yhteyshenkilö, puhelin, sahkoposti,
+ laskutusosoite, laskutuspostinumero, laskutuskaupunki, laskutussahkoposti,
+ elaskuosoite, elaskuvalittaja, ytunnus, lisatiedot, luotu, muokattu, muokkaaja)
+ VALUES (:id, :company_id, :yritys, :yhteyshenkilö, :puhelin, :sahkoposti,
+ :laskutusosoite, :laskutuspostinumero, :laskutuskaupunki, :laskutussahkoposti,
+ :elaskuosoite, :elaskuvalittaja, :ytunnus, :lisatiedot, :luotu, :muokattu, :muokkaaja)
+ ON DUPLICATE KEY UPDATE
+ yritys = VALUES(yritys), yhteyshenkilö = VALUES(yhteyshenkilö),
+ puhelin = VALUES(puhelin), sahkoposti = VALUES(sahkoposti),
+ laskutusosoite = VALUES(laskutusosoite), laskutuspostinumero = VALUES(laskutuspostinumero),
+ laskutuskaupunki = VALUES(laskutuskaupunki), laskutussahkoposti = VALUES(laskutussahkoposti),
+ elaskuosoite = VALUES(elaskuosoite), elaskuvalittaja = VALUES(elaskuvalittaja),
+ ytunnus = VALUES(ytunnus), lisatiedot = VALUES(lisatiedot),
+ muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja)
+ ");
+ $stmt->execute([
+ 'id' => $customer['id'],
+ 'company_id' => $companyId,
+ 'yritys' => $customer['yritys'] ?? '',
+ 'yhteyshenkilö' => $customer['yhteyshenkilö'] ?? '',
+ 'puhelin' => $customer['puhelin'] ?? '',
+ 'sahkoposti' => $customer['sahkoposti'] ?? '',
+ 'laskutusosoite' => $customer['laskutusosoite'] ?? '',
+ 'laskutuspostinumero' => $customer['laskutuspostinumero'] ?? '',
+ 'laskutuskaupunki' => $customer['laskutuskaupunki'] ?? '',
+ 'laskutussahkoposti' => $customer['laskutussahkoposti'] ?? '',
+ 'elaskuosoite' => $customer['elaskuosoite'] ?? '',
+ 'elaskuvalittaja' => $customer['elaskuvalittaja'] ?? '',
+ 'ytunnus' => $customer['ytunnus'] ?? '',
+ 'lisatiedot' => $customer['lisatiedot'] ?? '',
+ 'luotu' => $customer['luotu'] ?? date('Y-m-d H:i:s'),
+ 'muokattu' => $customer['muokattu'] ?? null,
+ 'muokkaaja' => $customer['muokkaaja'] ?? '',
+ ]);
+
+ // Päivitä liittymät
+ $db->prepare("DELETE FROM customer_connections WHERE customer_id = ?")->execute([$customer['id']]);
+ if (!empty($customer['liittymat'])) {
+ $ins = $db->prepare("
+ INSERT INTO customer_connections (customer_id, asennusosoite, postinumero, kaupunki, liittymanopeus, hinta, sopimuskausi, alkupvm)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ ");
+ foreach ($customer['liittymat'] as $l) {
+ $ins->execute([
+ $customer['id'],
+ $l['asennusosoite'] ?? '',
+ $l['postinumero'] ?? '',
+ $l['kaupunki'] ?? '',
+ $l['liittymanopeus'] ?? '',
+ $l['hinta'] ?? 0,
+ $l['sopimuskausi'] ?? '',
+ $l['alkupvm'] ?? '',
+ ]);
+ }
+ }
+ $db->commit();
+ } catch (Exception $e) {
+ $db->rollBack();
+ throw $e;
+ }
+}
+
+function dbDeleteCustomer(string $customerId): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM customers WHERE id = ?")->execute([$customerId]);
+}
+
+// ==================== LIIDIT ====================
+
+function dbLoadLeads(string $companyId): array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM leads WHERE company_id = ? ORDER BY luotu DESC");
+ $stmt->execute([$companyId]);
+ $leads = $stmt->fetchAll();
+ foreach ($leads as &$l) {
+ unset($l['company_id']);
+ }
+ return $leads;
+}
+
+function dbSaveLead(string $companyId, array $lead): void {
+ $db = getDb();
+ $stmt = $db->prepare("
+ INSERT INTO leads (id, company_id, yritys, yhteyshenkilo, puhelin, sahkoposti, osoite, kaupunki, tila, muistiinpanot, luotu, luoja, muokattu, muokkaaja)
+ VALUES (:id, :company_id, :yritys, :yhteyshenkilo, :puhelin, :sahkoposti, :osoite, :kaupunki, :tila, :muistiinpanot, :luotu, :luoja, :muokattu, :muokkaaja)
+ ON DUPLICATE KEY UPDATE
+ yritys = VALUES(yritys), yhteyshenkilo = VALUES(yhteyshenkilo),
+ puhelin = VALUES(puhelin), sahkoposti = VALUES(sahkoposti),
+ osoite = VALUES(osoite), kaupunki = VALUES(kaupunki),
+ tila = VALUES(tila), muistiinpanot = VALUES(muistiinpanot),
+ muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja)
+ ");
+ $stmt->execute([
+ 'id' => $lead['id'],
+ 'company_id' => $companyId,
+ 'yritys' => $lead['yritys'] ?? '',
+ 'yhteyshenkilo' => $lead['yhteyshenkilo'] ?? '',
+ 'puhelin' => $lead['puhelin'] ?? '',
+ 'sahkoposti' => $lead['sahkoposti'] ?? '',
+ 'osoite' => $lead['osoite'] ?? '',
+ 'kaupunki' => $lead['kaupunki'] ?? '',
+ 'tila' => $lead['tila'] ?? 'uusi',
+ 'muistiinpanot' => $lead['muistiinpanot'] ?? '',
+ 'luotu' => $lead['luotu'] ?? date('Y-m-d H:i:s'),
+ 'luoja' => $lead['luoja'] ?? '',
+ 'muokattu' => $lead['muokattu'] ?? null,
+ 'muokkaaja' => $lead['muokkaaja'] ?? '',
+ ]);
+}
+
+function dbDeleteLead(string $leadId): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM leads WHERE id = ?")->execute([$leadId]);
+}
+
+// ==================== TIKETIT ====================
+
+function dbLoadTickets(string $companyId): array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM tickets WHERE company_id = ? ORDER BY updated DESC");
+ $stmt->execute([$companyId]);
+ $tickets = $stmt->fetchAll();
+
+ $msgStmt = $db->prepare("SELECT * FROM ticket_messages WHERE ticket_id = ? ORDER BY timestamp");
+ $tagStmt = $db->prepare("SELECT tag FROM ticket_tags WHERE ticket_id = ?");
+
+ foreach ($tickets as &$t) {
+ // Viestit
+ $msgStmt->execute([$t['id']]);
+ $t['messages'] = array_map(function($m) {
+ return [
+ 'id' => $m['id'],
+ 'type' => $m['type'],
+ 'from' => $m['from_email'],
+ 'from_name' => $m['from_name'],
+ 'body' => $m['body'],
+ 'timestamp' => $m['timestamp'],
+ 'message_id' => $m['message_id'] ?? '',
+ ];
+ }, $msgStmt->fetchAll());
+
+ // Tagit
+ $tagStmt->execute([$t['id']]);
+ $t['tags'] = $tagStmt->fetchAll(PDO::FETCH_COLUMN);
+
+ unset($t['company_id']);
+ }
+ return $tickets;
+}
+
+function dbSaveTicket(string $companyId, array $ticket): void {
+ $db = getDb();
+ $db->beginTransaction();
+ try {
+ $stmt = $db->prepare("
+ INSERT INTO tickets (id, company_id, subject, from_email, from_name, status, type,
+ assigned_to, customer_id, customer_name, message_id, mailbox_id, auto_close_at, created, updated)
+ VALUES (:id, :company_id, :subject, :from_email, :from_name, :status, :type,
+ :assigned_to, :customer_id, :customer_name, :message_id, :mailbox_id, :auto_close_at, :created, :updated)
+ ON DUPLICATE KEY UPDATE
+ subject = VALUES(subject), from_email = VALUES(from_email), from_name = VALUES(from_name),
+ status = VALUES(status), type = VALUES(type), assigned_to = VALUES(assigned_to),
+ customer_id = VALUES(customer_id), customer_name = VALUES(customer_name),
+ message_id = VALUES(message_id), mailbox_id = VALUES(mailbox_id),
+ auto_close_at = VALUES(auto_close_at), updated = VALUES(updated)
+ ");
+ $stmt->execute([
+ 'id' => $ticket['id'],
+ 'company_id' => $companyId,
+ 'subject' => $ticket['subject'] ?? '',
+ 'from_email' => $ticket['from_email'] ?? '',
+ 'from_name' => $ticket['from_name'] ?? '',
+ 'status' => $ticket['status'] ?? 'uusi',
+ 'type' => $ticket['type'] ?? 'muu',
+ 'assigned_to' => $ticket['assigned_to'] ?? '',
+ 'customer_id' => $ticket['customer_id'] ?? '',
+ 'customer_name' => $ticket['customer_name'] ?? '',
+ 'message_id' => $ticket['message_id'] ?? '',
+ 'mailbox_id' => $ticket['mailbox_id'] ?? '',
+ 'auto_close_at' => $ticket['auto_close_at'] ?? '',
+ 'created' => $ticket['created'] ?? date('Y-m-d H:i:s'),
+ 'updated' => $ticket['updated'] ?? date('Y-m-d H:i:s'),
+ ]);
+
+ // Viestit — lisää vain uudet (ei poista vanhoja)
+ if (!empty($ticket['messages'])) {
+ $ins = $db->prepare("
+ INSERT IGNORE INTO ticket_messages (id, ticket_id, type, from_email, from_name, body, timestamp, message_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ ");
+ foreach ($ticket['messages'] as $m) {
+ $ins->execute([
+ $m['id'],
+ $ticket['id'],
+ $m['type'],
+ $m['from'] ?? $m['from_email'] ?? '',
+ $m['from_name'] ?? '',
+ $m['body'] ?? '',
+ $m['timestamp'] ?? date('Y-m-d H:i:s'),
+ $m['message_id'] ?? '',
+ ]);
+ }
+ }
+
+ // Tagit — korvaa kaikki
+ $db->prepare("DELETE FROM ticket_tags WHERE ticket_id = ?")->execute([$ticket['id']]);
+ if (!empty($ticket['tags'])) {
+ $ins = $db->prepare("INSERT INTO ticket_tags (ticket_id, tag) VALUES (?, ?)");
+ foreach ($ticket['tags'] as $tag) {
+ if ($tag) $ins->execute([$ticket['id'], $tag]);
+ }
+ }
+
+ $db->commit();
+ } catch (Exception $e) {
+ $db->rollBack();
+ throw $e;
+ }
+}
+
+function dbDeleteTicket(string $ticketId): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM tickets WHERE id = ?")->execute([$ticketId]);
+}
+
+function dbFindTicketByMessageId(string $companyId, string $messageId): ?array {
+ $db = getDb();
+ // Ensin tikettitasolla
+ $stmt = $db->prepare("SELECT * FROM tickets WHERE company_id = ? AND message_id = ? LIMIT 1");
+ $stmt->execute([$companyId, $messageId]);
+ $t = $stmt->fetch();
+ if ($t) return $t;
+
+ // Sitten viestien message_id:stä
+ $stmt = $db->prepare("
+ SELECT t.* FROM tickets t
+ JOIN ticket_messages tm ON t.id = tm.ticket_id
+ WHERE t.company_id = ? AND tm.message_id = ?
+ LIMIT 1
+ ");
+ $stmt->execute([$companyId, $messageId]);
+ return $stmt->fetch() ?: null;
+}
+
+// ==================== ARKISTO ====================
+
+function dbLoadArchive(string $companyId): array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM archives WHERE company_id = ? ORDER BY archived_at DESC");
+ $stmt->execute([$companyId]);
+ $rows = $stmt->fetchAll();
+ return array_map(function($row) {
+ $data = json_decode($row['data'], true) ?? [];
+ $data['id'] = $row['id'];
+ $data['archived_at'] = $row['archived_at'];
+ return $data;
+ }, $rows);
+}
+
+function dbArchiveCustomer(string $companyId, array $customerData): void {
+ $db = getDb();
+ $stmt = $db->prepare("INSERT INTO archives (id, company_id, data, archived_at) VALUES (?, ?, ?, NOW())");
+ $stmt->execute([$customerData['id'], $companyId, json_encode($customerData, JSON_UNESCAPED_UNICODE)]);
+}
+
+function dbRestoreArchive(string $archiveId): ?array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM archives WHERE id = ?");
+ $stmt->execute([$archiveId]);
+ $row = $stmt->fetch();
+ if (!$row) return null;
+ $db->prepare("DELETE FROM archives WHERE id = ?")->execute([$archiveId]);
+ return json_decode($row['data'], true);
+}
+
+function dbDeleteArchive(string $archiveId): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM archives WHERE id = ?")->execute([$archiveId]);
+}
+
+// ==================== CHANGELOG ====================
+
+function dbAddLog(string $companyId, string $user, string $action, string $customerId = '', string $customerName = '', string $details = ''): void {
+ if (empty($companyId)) return;
+ $db = getDb();
+
+ $id = bin2hex(random_bytes(8));
+ $stmt = $db->prepare("
+ INSERT INTO changelog (id, company_id, timestamp, user, action, customer_id, customer_name, details)
+ VALUES (?, ?, NOW(), ?, ?, ?, ?, ?)
+ ");
+ $stmt->execute([$id, $companyId, $user, $action, $customerId, $customerName, $details]);
+
+ // Pidä max 500 per yritys
+ $db->prepare("
+ DELETE FROM changelog WHERE company_id = ? AND id NOT IN (
+ SELECT id FROM (SELECT id FROM changelog WHERE company_id = ? ORDER BY timestamp DESC LIMIT 500) tmp
+ )
+ ")->execute([$companyId, $companyId]);
+}
+
+function dbLoadChangelog(string $companyId, int $limit = 100): array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM changelog WHERE company_id = ? ORDER BY timestamp DESC LIMIT ?");
+ $stmt->execute([$companyId, $limit]);
+ return $stmt->fetchAll();
+}
+
+// ==================== POSTILAATIKOT ====================
+
+function dbLoadMailboxes(string $companyId): array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM mailboxes WHERE company_id = ?");
+ $stmt->execute([$companyId]);
+ $boxes = $stmt->fetchAll();
+ foreach ($boxes as &$b) {
+ $b['aktiivinen'] = (bool)$b['aktiivinen'];
+ $b['imap_port'] = (int)$b['imap_port'];
+ unset($b['company_id']);
+ }
+ return $boxes;
+}
+
+function dbSaveMailbox(string $companyId, array $mailbox): void {
+ $db = getDb();
+ $stmt = $db->prepare("
+ INSERT INTO mailboxes (id, company_id, nimi, imap_host, imap_port, imap_user, imap_encryption, imap_password, smtp_from_email, smtp_from_name, aktiivinen)
+ VALUES (:id, :company_id, :nimi, :imap_host, :imap_port, :imap_user, :imap_encryption, :imap_password, :smtp_from_email, :smtp_from_name, :aktiivinen)
+ ON DUPLICATE KEY UPDATE
+ nimi = VALUES(nimi), imap_host = VALUES(imap_host), imap_port = VALUES(imap_port),
+ imap_user = VALUES(imap_user), imap_encryption = VALUES(imap_encryption),
+ imap_password = VALUES(imap_password), smtp_from_email = VALUES(smtp_from_email),
+ smtp_from_name = VALUES(smtp_from_name), aktiivinen = VALUES(aktiivinen)
+ ");
+ $stmt->execute([
+ 'id' => $mailbox['id'],
+ 'company_id' => $companyId,
+ 'nimi' => $mailbox['nimi'] ?? '',
+ 'imap_host' => $mailbox['imap_host'] ?? '',
+ 'imap_port' => $mailbox['imap_port'] ?? 993,
+ 'imap_user' => $mailbox['imap_user'] ?? '',
+ 'imap_encryption' => $mailbox['imap_encryption'] ?? 'ssl',
+ 'imap_password' => $mailbox['imap_password'] ?? '',
+ 'smtp_from_email' => $mailbox['smtp_from_email'] ?? '',
+ 'smtp_from_name' => $mailbox['smtp_from_name'] ?? '',
+ 'aktiivinen' => $mailbox['aktiivinen'] ?? true,
+ ]);
+}
+
+function dbDeleteMailbox(string $mailboxId): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM mailboxes WHERE id = ?")->execute([$mailboxId]);
+}
+
+function dbGetMailbox(string $mailboxId): ?array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM mailboxes WHERE id = ?");
+ $stmt->execute([$mailboxId]);
+ $b = $stmt->fetch();
+ if ($b) {
+ $b['aktiivinen'] = (bool)$b['aktiivinen'];
+ $b['imap_port'] = (int)$b['imap_port'];
+ }
+ return $b ?: null;
+}
+
+// ==================== TIKETTISÄÄNNÖT ====================
+
+function dbLoadTicketRules(string $companyId): array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT * FROM ticket_rules WHERE company_id = ? ORDER BY priority");
+ $stmt->execute([$companyId]);
+ $rules = $stmt->fetchAll();
+ foreach ($rules as &$r) {
+ $r['priority'] = (int)$r['priority'];
+ $r['auto_close_days'] = (int)$r['auto_close_days'];
+ unset($r['company_id']);
+ }
+ return $rules;
+}
+
+function dbSaveTicketRule(string $companyId, array $rule): void {
+ $db = getDb();
+ $stmt = $db->prepare("
+ INSERT INTO ticket_rules (id, company_id, name, from_contains, priority, tag, assign_to, status_set, type_set, auto_close_days)
+ VALUES (:id, :company_id, :name, :from_contains, :priority, :tag, :assign_to, :status_set, :type_set, :auto_close_days)
+ ON DUPLICATE KEY UPDATE
+ name = VALUES(name), from_contains = VALUES(from_contains), priority = VALUES(priority),
+ tag = VALUES(tag), assign_to = VALUES(assign_to), status_set = VALUES(status_set),
+ type_set = VALUES(type_set), auto_close_days = VALUES(auto_close_days)
+ ");
+ $stmt->execute([
+ 'id' => $rule['id'],
+ 'company_id' => $companyId,
+ 'name' => $rule['name'] ?? '',
+ 'from_contains' => $rule['from_contains'] ?? '',
+ 'priority' => $rule['priority'] ?? 0,
+ 'tag' => $rule['tag'] ?? '',
+ 'assign_to' => $rule['assign_to'] ?? '',
+ 'status_set' => $rule['status_set'] ?? '',
+ 'type_set' => $rule['type_set'] ?? '',
+ 'auto_close_days' => $rule['auto_close_days'] ?? 0,
+ ]);
+}
+
+function dbDeleteTicketRule(string $ruleId): void {
+ $db = getDb();
+ $db->prepare("DELETE FROM ticket_rules WHERE id = ?")->execute([$ruleId]);
+}
+
+// ==================== YRITYKSEN API-ASETUKSET ====================
+
+function dbGetCompanyConfig(string $companyId): array {
+ return [
+ 'mailboxes' => dbLoadMailboxes($companyId),
+ 'ticket_rules' => dbLoadTicketRules($companyId),
+ 'api_key' => dbGetCompanyApiKey($companyId),
+ 'cors_origins' => dbGetCompanyCorsOrigins($companyId),
+ ];
+}
+
+function dbGetCompanyApiKey(string $companyId): string {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT api_key FROM companies WHERE id = ?");
+ $stmt->execute([$companyId]);
+ return $stmt->fetchColumn() ?: '';
+}
+
+function dbGetCompanyCorsOrigins(string $companyId): array {
+ $db = getDb();
+ $stmt = $db->prepare("SELECT cors_origins FROM companies WHERE id = ?");
+ $stmt->execute([$companyId]);
+ $val = $stmt->fetchColumn();
+ if (!$val) return [];
+ $decoded = json_decode($val, true);
+ return is_array($decoded) ? $decoded : [];
+}
+
+function dbSetCompanyApiKey(string $companyId, string $apiKey): void {
+ $db = getDb();
+ $db->prepare("UPDATE companies SET api_key = ? WHERE id = ?")->execute([$apiKey, $companyId]);
+}
+
+function dbSetCompanyCorsOrigins(string $companyId, array $origins): void {
+ $db = getDb();
+ $db->prepare("UPDATE companies SET cors_origins = ? WHERE id = ?")->execute([json_encode($origins), $companyId]);
+}
diff --git a/migrate.php b/migrate.php
new file mode 100644
index 0000000..c548544
--- /dev/null
+++ b/migrate.php
@@ -0,0 +1,200 @@
+prepare("INSERT IGNORE INTO reset_tokens (user_id, token, created_at) VALUES (?, ?, ?)");
+ $expires = $t['expires'] ?? time();
+ $created = date('Y-m-d H:i:s', $expires - 3600); // Arvio luontiajasta
+ $stmt->execute([$t['user_id'], $t['token'], $created]);
+ } catch (Exception $e) {
+ echo " ⚠ Token ohitettu: " . $e->getMessage() . "\n";
+ }
+ }
+ echo " ✓ " . count($tokens) . " tokenia\n";
+}
+echo "\n";
+
+// 6. Yrityskohtainen data
+echo "6. Siirretään yrityskohtainen data...\n";
+$companiesDir = $dataDir . '/companies';
+if (is_dir($companiesDir)) {
+ $dirs = array_filter(scandir($companiesDir), fn($d) => $d !== '.' && $d !== '..' && is_dir("$companiesDir/$d"));
+
+ foreach ($dirs as $companyId) {
+ $compDir = "$companiesDir/$companyId";
+ echo "\n === $companyId ===\n";
+
+ // Yrityksen config (postilaatikot, tikettisäännöt, api_key, cors_origins)
+ $compConfig = "$compDir/config.json";
+ if (file_exists($compConfig)) {
+ $cfg = json_decode(file_get_contents($compConfig), true) ?: [];
+
+ // Postilaatikot
+ $mailboxes = $cfg['mailboxes'] ?? [];
+ foreach ($mailboxes as $mb) {
+ dbSaveMailbox($companyId, $mb);
+ }
+ echo " ✓ " . count($mailboxes) . " postilaatikkoa\n";
+
+ // Tikettisäännöt
+ $rules = $cfg['ticket_rules'] ?? [];
+ foreach ($rules as $rule) {
+ dbSaveTicketRule($companyId, $rule);
+ }
+ echo " ✓ " . count($rules) . " tikettisääntöä\n";
+
+ // API-avain ja CORS
+ if (!empty($cfg['api_key'])) {
+ dbSetCompanyApiKey($companyId, $cfg['api_key']);
+ echo " ✓ API-avain\n";
+ }
+ if (!empty($cfg['cors_origins'])) {
+ dbSetCompanyCorsOrigins($companyId, $cfg['cors_origins']);
+ echo " ✓ CORS-asetukset\n";
+ }
+ }
+
+ // Asiakkaat
+ $custFile = "$compDir/customers.json";
+ if (file_exists($custFile)) {
+ $customers = json_decode(file_get_contents($custFile), true) ?: [];
+ foreach ($customers as $cust) {
+ // Migroi vanhat flat-kentät liittymiksi jos tarpeen
+ if (!isset($cust['liittymat'])) {
+ $cust['liittymat'] = [];
+ if (!empty($cust['asennusosoite'] ?? '') || !empty($cust['liittymanopeus'] ?? '')) {
+ $cust['liittymat'][] = [
+ 'asennusosoite' => $cust['asennusosoite'] ?? '',
+ 'postinumero' => $cust['postinumero'] ?? '',
+ 'kaupunki' => $cust['kaupunki'] ?? '',
+ 'liittymanopeus' => $cust['liittymanopeus'] ?? '',
+ 'hinta' => $cust['hinta'] ?? 0,
+ 'sopimuskausi' => $cust['sopimuskausi'] ?? '',
+ 'alkupvm' => $cust['alkupvm'] ?? '',
+ ];
+ }
+ }
+ dbSaveCustomer($companyId, $cust);
+ }
+ echo " ✓ " . count($customers) . " asiakasta\n";
+ }
+
+ // Liidit
+ $leadsFile = "$compDir/leads.json";
+ if (file_exists($leadsFile)) {
+ $leads = json_decode(file_get_contents($leadsFile), true) ?: [];
+ foreach ($leads as $lead) {
+ dbSaveLead($companyId, $lead);
+ }
+ echo " ✓ " . count($leads) . " liidiä\n";
+ }
+
+ // Tiketit
+ $ticketsFile = "$compDir/tickets.json";
+ if (file_exists($ticketsFile)) {
+ $tickets = json_decode(file_get_contents($ticketsFile), true) ?: [];
+ foreach ($tickets as $ticket) {
+ dbSaveTicket($companyId, $ticket);
+ }
+ echo " ✓ " . count($tickets) . " tikettiä\n";
+ }
+
+ // Arkisto
+ $archiveFile = "$compDir/archive.json";
+ if (file_exists($archiveFile)) {
+ $archives = json_decode(file_get_contents($archiveFile), true) ?: [];
+ foreach ($archives as $arc) {
+ if (!empty($arc['id'])) {
+ dbArchiveCustomer($companyId, $arc);
+ }
+ }
+ echo " ✓ " . count($archives) . " arkistoitua\n";
+ }
+
+ // Changelog
+ $logFile = "$compDir/changelog.json";
+ if (file_exists($logFile)) {
+ $logs = json_decode(file_get_contents($logFile), true) ?: [];
+ $db = getDb();
+ $stmt = $db->prepare("
+ INSERT IGNORE INTO changelog (id, company_id, timestamp, user, action, customer_id, customer_name, details)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ ");
+ foreach ($logs as $log) {
+ $stmt->execute([
+ $log['id'] ?? bin2hex(random_bytes(8)),
+ $companyId,
+ $log['timestamp'] ?? date('Y-m-d H:i:s'),
+ $log['user'] ?? '',
+ $log['action'] ?? '',
+ $log['customer_id'] ?? '',
+ $log['customer_name'] ?? '',
+ $log['details'] ?? '',
+ ]);
+ }
+ echo " ✓ " . count($logs) . " lokimerkintää\n";
+ }
+ }
+}
+
+echo "\n=== Migraatio valmis! ===\n";
+echo "\nSeuraavat vaiheet:\n";
+echo "1. Testaa: php -r \"require 'db.php'; print_r(dbLoadCompanies());\"\n";
+echo "2. Ota MySQL-pohjainen api.php käyttöön\n";