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";