set_charset($cfg['charset'] ?? 'utf8mb4'); } return $db; } // ==================== HELPER-FUNKTIOT ==================== /** * Valmistele ja suorita kysely. Tukee sekä nimettyja (:name) että positiivisia (?) parametreja. */ function _dbRun(string $sql, array $params = []): mysqli_stmt { $db = getDb(); // Muunna nimetyt parametrit (:name) positiivisiksi (?) if (!empty($params) && !array_is_list($params)) { $ordered = []; $sql = preg_replace_callback('/:(\w+)/u', function($m) use ($params, &$ordered) { $key = $m[1]; $ordered[] = array_key_exists($key, $params) ? $params[$key] : null; return '?'; }, $sql); $params = $ordered; } $stmt = $db->prepare($sql); if ($stmt === false) { throw new RuntimeException("MySQL prepare failed: " . $db->error); } if (!empty($params)) { $types = ''; foreach ($params as $p) { if (is_int($p)) $types .= 'i'; elseif (is_float($p)) $types .= 'd'; elseif (is_bool($p)) $types .= 'i'; else $types .= 's'; } // Booleanit → int $vals = array_map(fn($v) => is_bool($v) ? (int)$v : $v, array_values($params)); $stmt->bind_param($types, ...$vals); } $stmt->execute(); return $stmt; } /** SELECT — kaikki rivit */ function _dbFetchAll(string $sql, array $params = []): array { $stmt = _dbRun($sql, $params); $result = $stmt->get_result(); return $result ? $result->fetch_all(MYSQLI_ASSOC) : []; } /** SELECT — yksi rivi */ function _dbFetchOne(string $sql, array $params = []): ?array { $stmt = _dbRun($sql, $params); $result = $stmt->get_result(); $row = $result ? $result->fetch_assoc() : null; return $row ?: null; } /** SELECT — yhden sarakkeen arvot */ function _dbFetchColumn(string $sql, array $params = []): array { $stmt = _dbRun($sql, $params); $result = $stmt->get_result(); if (!$result) return []; $col = []; while ($row = $result->fetch_row()) { $col[] = $row[0]; } return $col; } /** SELECT — yksittäinen skalaariarvo */ function _dbFetchScalar(string $sql, array $params = []) { $stmt = _dbRun($sql, $params); $result = $stmt->get_result(); $row = $result ? $result->fetch_row() : null; return $row ? $row[0] : null; } /** INSERT/UPDATE/DELETE — suorita, palauta affected_rows */ function _dbExecute(string $sql, array $params = []): int { $stmt = _dbRun($sql, $params); return $stmt->affected_rows; } // ==================== TAULUJEN LUONTI ==================== function initDatabase(): void { $db = getDb(); $tables = [ "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('superadmin','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 DEFAULT CHARSET=utf8mb4", "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 user_hidden_mailboxes ( user_id VARCHAR(20) NOT NULL, mailbox_id VARCHAR(20) NOT NULL, PRIMARY KEY (user_id, mailbox_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) 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 DEFAULT CHARSET=utf8mb4", "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 DEFAULT CHARSET=utf8mb4", "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, priority_emails TEXT DEFAULT '', 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, ticket_number INT DEFAULT NULL, 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 '', cc TEXT DEFAULT '', priority VARCHAR(20) DEFAULT 'normaali', 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_host VARCHAR(255) DEFAULT '', smtp_port INT DEFAULT 587, smtp_user VARCHAR(255) DEFAULT '', smtp_password VARCHAR(255) DEFAULT '', smtp_encryption VARCHAR(10) DEFAULT 'tls', smtp_from_email VARCHAR(255), smtp_from_name VARCHAR(255), aktiivinen BOOLEAN DEFAULT TRUE, auto_reply_enabled BOOLEAN DEFAULT FALSE, auto_reply_body TEXT, 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 ticket_types ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, value VARCHAR(50) NOT NULL, label VARCHAR(100) NOT NULL, color VARCHAR(20) DEFAULT '', sort_order INT DEFAULT 0, UNIQUE KEY uk_company_value (company_id, value), 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 fetched_message_ids ( id INT AUTO_INCREMENT PRIMARY KEY, company_id VARCHAR(50) NOT NULL, message_id VARCHAR(512) NOT NULL, fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_company_msgid (company_id, message_id), 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_priority_emails ( id INT AUTO_INCREMENT PRIMARY KEY, company_id VARCHAR(50) NOT NULL, email VARCHAR(255) NOT NULL, FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, INDEX idx_company (company_id), UNIQUE KEY udx_company_email (company_id, email) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS reply_templates ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, nimi VARCHAR(255) NOT NULL, body TEXT NOT NULL, sort_order 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 sites ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, nimi VARCHAR(255) NOT NULL, osoite VARCHAR(255) DEFAULT '', kaupunki 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 devices ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, nimi VARCHAR(255) NOT NULL, hallintaosoite VARCHAR(255) DEFAULT '', serial VARCHAR(255) DEFAULT '', site_id VARCHAR(20) NULL, funktio VARCHAR(255) DEFAULT '', tyyppi VARCHAR(100) DEFAULT '', malli VARCHAR(255) DEFAULT '', ping_check BOOLEAN DEFAULT FALSE, ping_status VARCHAR(20) DEFAULT '', ping_checked_at DATETIME NULL, 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 ipam ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, tyyppi VARCHAR(20) NOT NULL DEFAULT 'ip', nimi VARCHAR(255) DEFAULT '', verkko VARCHAR(50) DEFAULT '', vlan_id INT DEFAULT NULL, site_id VARCHAR(20) NULL, tila VARCHAR(20) DEFAULT 'vapaa', asiakas VARCHAR(255) DEFAULT '', 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 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", "CREATE TABLE IF NOT EXISTS guide_categories ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, nimi VARCHAR(255) NOT NULL, sort_order 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 guides ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, category_id VARCHAR(20) DEFAULT NULL, title VARCHAR(500) NOT NULL, content LONGTEXT, tags VARCHAR(500) DEFAULT '', author VARCHAR(100) DEFAULT '', pinned TINYINT(1) DEFAULT 0, luotu DATETIME, muokattu DATETIME NULL, muokkaaja VARCHAR(100) DEFAULT '', FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, INDEX idx_company (company_id), INDEX idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS todos ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, type VARCHAR(20) NOT NULL DEFAULT 'task', title VARCHAR(500) NOT NULL, description TEXT, status VARCHAR(30) NOT NULL DEFAULT 'avoin', priority VARCHAR(20) DEFAULT 'normaali', category VARCHAR(30) DEFAULT '', assigned_to VARCHAR(100) DEFAULT '', created_by VARCHAR(100) NOT NULL DEFAULT '', deadline DATE DEFAULT NULL, luotu DATETIME, muokattu DATETIME NULL, muokkaaja VARCHAR(100) DEFAULT '', FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, INDEX idx_company (company_id), INDEX idx_type (type), INDEX idx_status (status), INDEX idx_deadline (deadline) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS todo_comments ( id VARCHAR(20) PRIMARY KEY, todo_id VARCHAR(20) NOT NULL, author VARCHAR(100) NOT NULL, body TEXT NOT NULL, luotu DATETIME, FOREIGN KEY (todo_id) REFERENCES todos(id) ON DELETE CASCADE, INDEX idx_todo (todo_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS todo_time_entries ( id VARCHAR(20) PRIMARY KEY, todo_id VARCHAR(20) NOT NULL, user VARCHAR(100) NOT NULL, hours DECIMAL(6,2) NOT NULL, description VARCHAR(500) DEFAULT '', work_date DATE NOT NULL, luotu DATETIME, FOREIGN KEY (todo_id) REFERENCES todos(id) ON DELETE CASCADE, INDEX idx_todo (todo_id), INDEX idx_user (user) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS todo_subtasks ( id VARCHAR(20) PRIMARY KEY, todo_id VARCHAR(20) NOT NULL, title VARCHAR(500) NOT NULL, completed TINYINT(1) DEFAULT 0, sort_order INT DEFAULT 0, created_by VARCHAR(100) DEFAULT '', luotu DATETIME, FOREIGN KEY (todo_id) REFERENCES todos(id) ON DELETE CASCADE, INDEX idx_todo (todo_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS documents ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, customer_id VARCHAR(20) DEFAULT NULL, title VARCHAR(255) NOT NULL, description TEXT DEFAULT '', category VARCHAR(50) DEFAULT 'muu', current_version INT DEFAULT 0, created_by VARCHAR(100) DEFAULT '', luotu DATETIME, muokattu DATETIME, muokkaaja VARCHAR(100) DEFAULT '', FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, INDEX idx_company (company_id), INDEX idx_customer (customer_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS document_versions ( id VARCHAR(20) PRIMARY KEY, document_id VARCHAR(20) NOT NULL, version_number INT NOT NULL, filename VARCHAR(255) NOT NULL, original_name VARCHAR(255) NOT NULL, file_size INT DEFAULT 0, mime_type VARCHAR(100) DEFAULT '', change_notes TEXT DEFAULT '', created_by VARCHAR(100) DEFAULT '', luotu DATETIME, FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE, INDEX idx_document (document_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS document_folders ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, name VARCHAR(255) NOT NULL, parent_id VARCHAR(20) DEFAULT NULL, created_by VARCHAR(100) DEFAULT '', luotu DATETIME, FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, INDEX idx_company (company_id), INDEX idx_parent (parent_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS laitetilat ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, nimi VARCHAR(255) NOT NULL, kuvaus TEXT DEFAULT '', osoite VARCHAR(255) DEFAULT '', luotu DATETIME, muokattu DATETIME, 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 laitetila_files ( id VARCHAR(20) PRIMARY KEY, laitetila_id VARCHAR(20) NOT NULL, filename VARCHAR(255) NOT NULL, original_name VARCHAR(255) NOT NULL, file_size INT DEFAULT 0, mime_type VARCHAR(100) DEFAULT '', description VARCHAR(500) DEFAULT '', created_by VARCHAR(100) DEFAULT '', luotu DATETIME, FOREIGN KEY (laitetila_id) REFERENCES laitetilat(id) ON DELETE CASCADE, INDEX idx_laitetila (laitetila_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS integrations ( id VARCHAR(20) PRIMARY KEY, company_id VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL, enabled BOOLEAN DEFAULT FALSE, config JSON, created DATETIME, updated DATETIME, UNIQUE KEY uk_company_type (company_id, type), INDEX idx_company (company_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci", "CREATE TABLE IF NOT EXISTS availability_queries ( id INT AUTO_INCREMENT PRIMARY KEY, company_id VARCHAR(50) NOT NULL, osoite VARCHAR(255) NOT NULL, postinumero VARCHAR(20) NOT NULL, kaupunki VARCHAR(100) NOT NULL, saatavilla BOOLEAN NOT NULL DEFAULT FALSE, ip_address VARCHAR(45) DEFAULT '', user_agent VARCHAR(500) DEFAULT '', referer VARCHAR(500) DEFAULT '', created_at DATETIME NOT NULL, INDEX idx_company (company_id), INDEX idx_created (created_at), INDEX idx_postinumero (postinumero), FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci", ]; foreach ($tables as $i => $sql) { if ($db->query($sql) === false) { throw new RuntimeException("Taulun luonti epäonnistui (taulu #" . ($i+1) . "): " . $db->error); } } // ALTER TABLE -migraatiot (turvallisia, ajetaan kerran) $alters = [ "ALTER TABLE tickets ADD COLUMN cc TEXT DEFAULT '' AFTER mailbox_id", "ALTER TABLE tickets ADD COLUMN priority VARCHAR(20) DEFAULT 'normaali' AFTER cc", "ALTER TABLE customers ADD COLUMN priority_emails TEXT DEFAULT '' AFTER lisatiedot", "ALTER TABLE companies ADD COLUMN enabled_modules TEXT DEFAULT '' AFTER cors_origins", "ALTER TABLE users MODIFY COLUMN role ENUM('superadmin','admin','user') DEFAULT 'user'", "ALTER TABLE customer_connections ADD COLUMN lisatiedot TEXT DEFAULT '' AFTER alkupvm", "ALTER TABLE customer_connections ADD COLUMN vlan VARCHAR(20) DEFAULT '' AFTER lisatiedot", "ALTER TABLE customer_connections ADD COLUMN laite VARCHAR(100) DEFAULT '' AFTER vlan", "ALTER TABLE customer_connections ADD COLUMN portti VARCHAR(100) DEFAULT '' AFTER laite", "ALTER TABLE customer_connections ADD COLUMN ip VARCHAR(100) DEFAULT '' AFTER portti", "ALTER TABLE mailboxes ADD COLUMN smtp_host VARCHAR(255) DEFAULT '' AFTER smtp_from_name", "ALTER TABLE mailboxes ADD COLUMN smtp_port INT DEFAULT 587 AFTER smtp_host", "ALTER TABLE mailboxes ADD COLUMN smtp_user VARCHAR(255) DEFAULT '' AFTER smtp_port", "ALTER TABLE mailboxes ADD COLUMN smtp_password VARCHAR(255) DEFAULT '' AFTER smtp_user", "ALTER TABLE mailboxes ADD COLUMN smtp_encryption VARCHAR(10) DEFAULT 'tls' AFTER smtp_password", "ALTER TABLE tickets ADD COLUMN ticket_number INT DEFAULT NULL AFTER id", "ALTER TABLE mailboxes ADD COLUMN auto_reply_enabled BOOLEAN DEFAULT FALSE AFTER aktiivinen", "ALTER TABLE mailboxes ADD COLUMN auto_reply_body TEXT AFTER auto_reply_enabled", "ALTER TABLE companies ADD COLUMN allowed_ips TEXT DEFAULT '' AFTER enabled_modules", "ALTER TABLE todos ADD COLUMN category VARCHAR(30) DEFAULT '' AFTER priority", "ALTER TABLE user_companies ADD COLUMN role VARCHAR(20) DEFAULT 'user' AFTER company_id", "ALTER TABLE documents ADD COLUMN folder_id VARCHAR(20) DEFAULT NULL AFTER customer_id", "ALTER TABLE documents ADD COLUMN max_versions INT DEFAULT 10 AFTER current_version", "ALTER TABLE document_versions ADD COLUMN content MEDIUMTEXT DEFAULT NULL AFTER mime_type", "ALTER TABLE devices ADD COLUMN laitetila_id VARCHAR(20) DEFAULT NULL AFTER site_id", "ALTER TABLE document_folders ADD COLUMN customer_id VARCHAR(20) DEFAULT NULL AFTER company_id", "ALTER TABLE customer_connections ADD COLUMN gateway_device_id VARCHAR(20) DEFAULT NULL AFTER ip", "ALTER TABLE ticket_rules ADD COLUMN subject_contains VARCHAR(255) DEFAULT '' AFTER from_contains", "ALTER TABLE ticket_rules ADD COLUMN to_contains VARCHAR(255) DEFAULT '' AFTER subject_contains", "ALTER TABLE ticket_rules ADD COLUMN enabled BOOLEAN DEFAULT TRUE AFTER auto_close_days", "ALTER TABLE ticket_rules ADD COLUMN set_priority VARCHAR(20) DEFAULT '' AFTER type_set", "ALTER TABLE ticket_rules ADD COLUMN set_tags VARCHAR(255) DEFAULT '' AFTER set_priority", "ALTER TABLE tickets ADD COLUMN zammad_ticket_id INT DEFAULT NULL AFTER mailbox_id", "ALTER TABLE ticket_messages ADD COLUMN zammad_article_id INT DEFAULT NULL AFTER message_id", "ALTER TABLE availability_queries ADD COLUMN hostname VARCHAR(255) DEFAULT '' AFTER ip_address", ]; foreach ($alters as $sql) { try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } } // Kertaluontoinen migraatio: päivitä vanhat admin-käyttäjät superadminiksi // (vain jos yhtään superadminia ei vielä ole) try { $result = $db->query("SELECT COUNT(*) AS cnt FROM users WHERE role = 'superadmin'"); $row = $result->fetch_assoc(); if ((int)($row['cnt'] ?? 0) === 0) { $db->query("UPDATE users SET role = 'superadmin' WHERE role = 'admin'"); } } catch (\Throwable $e) { /* ohitetaan */ } // Migraatio: kopioi admin-käyttäjien rooli user_companies-tauluun // (kun role-sarake lisätty, olemassa olevat admin-käyttäjät saavat admin-roolin kaikkiin yrityksiinsä) try { $db->query("UPDATE user_companies uc JOIN users u ON u.id = uc.user_id SET uc.role = 'admin' WHERE u.role = 'admin' AND uc.role = 'user'"); } catch (\Throwable $e) { /* ohitetaan */ } // Migraatio: muuta globaali 'admin' → 'user' (admin on nyt yrityskohtainen user_companies.role) try { $db->query("UPDATE users SET role = 'user' WHERE role = 'admin'"); } catch (\Throwable $e) { /* ohitetaan */ } // Migraatio: yhdistä sites → laitetilat (kopioi vanhat sijainnit laitetiloiksi) try { $sitesExist = $db->query("SELECT COUNT(*) AS cnt FROM sites")->fetch_assoc(); if ((int)($sitesExist['cnt'] ?? 0) > 0) { // Kopioi sites-taulun rivit laitetilat-tauluun (ohita duplikaatit) $db->query(" INSERT IGNORE INTO laitetilat (id, company_id, nimi, kuvaus, osoite, luotu, muokattu, muokkaaja) SELECT id, company_id, nimi, '', CONCAT(IFNULL(osoite,''), IF(kaupunki != '', CONCAT(', ', kaupunki), '')), NOW(), NOW(), '' FROM sites "); // Päivitä laitteiden laitetila_id vanhoista site_id-viittauksista $db->query("UPDATE devices SET laitetila_id = site_id WHERE laitetila_id IS NULL AND site_id IS NOT NULL"); // Tyhjennä sites-taulu ettei migraatio toista itseään $db->query("DELETE FROM sites"); } } catch (\Throwable $e) { /* ohitetaan */ } // Migraatio: aseta vanhojen kansioiden customer_id dokumenttien perusteella try { $orphans = $db->query("SELECT id FROM document_folders WHERE customer_id IS NULL"); while ($row = $orphans->fetch_assoc()) { $fid = $row['id']; // Hae yleisin customer_id kansion dokumenteista $res = $db->query("SELECT customer_id, COUNT(*) AS cnt FROM documents WHERE folder_id = '$fid' AND customer_id IS NOT NULL GROUP BY customer_id ORDER BY cnt DESC LIMIT 1"); $top = $res->fetch_assoc(); if ($top && $top['customer_id']) { $cid = $db->real_escape_string($top['customer_id']); $db->query("UPDATE document_folders SET customer_id = '$cid' WHERE id = '$fid'"); } else { // Kansio ei sisällä dokumentteja asiakkaalla → poistetaan $db->query("UPDATE documents SET folder_id = NULL WHERE folder_id = '$fid'"); $db->query("DELETE FROM document_folders WHERE id = '$fid'"); } } } catch (\Throwable $e) { /* ohitetaan */ } // Migraatio: täytä fetched_message_ids olemassaolevien tikettien message_id:illä // (ajetaan vain kerran — kun taulu on tyhjä) try { $cnt = $db->query("SELECT COUNT(*) AS cnt FROM fetched_message_ids")->fetch_assoc(); if ((int)($cnt['cnt'] ?? 0) === 0) { // Tikettien omat message_id:t $db->query("INSERT IGNORE INTO fetched_message_ids (company_id, message_id) SELECT company_id, message_id FROM tickets WHERE message_id IS NOT NULL AND message_id != ''"); // Tikettien viestien message_id:t (JSON messages-kentästä) $rows = $db->query("SELECT company_id, messages FROM tickets WHERE messages IS NOT NULL AND messages != ''"); while ($row = $rows->fetch_assoc()) { $msgs = json_decode($row['messages'], true); if (is_array($msgs)) { foreach ($msgs as $m) { if (!empty($m['message_id'])) { $cid = $db->real_escape_string($row['company_id']); $mid = $db->real_escape_string($m['message_id']); $db->query("INSERT IGNORE INTO fetched_message_ids (company_id, message_id) VALUES ('$cid', '$mid')"); } } } } } } catch (\Throwable $e) { /* ohitetaan */ } } // ==================== YRITYKSET ==================== function dbLoadCompanies(): array { $companies = _dbFetchAll("SELECT * FROM companies ORDER BY nimi"); foreach ($companies as &$c) { $c['domains'] = _dbFetchColumn("SELECT domain FROM company_domains WHERE company_id = ?", [$c['id']]); $c['aktiivinen'] = (bool)$c['aktiivinen']; // enabled_modules: JSON-array tai tyhjä (= kaikki päällä) $raw = $c['enabled_modules'] ?? ''; $c['enabled_modules'] = $raw ? (json_decode($raw, true) ?: []) : []; $c['allowed_ips'] = $c['allowed_ips'] ?? ''; } return $companies; } function dbSaveCompany(array $company): void { $db = getDb(); $db->begin_transaction(); try { $enabledModules = $company['enabled_modules'] ?? []; $enabledModulesJson = is_array($enabledModules) ? json_encode($enabledModules) : ($enabledModules ?: ''); _dbExecute(" INSERT INTO companies (id, nimi, luotu, aktiivinen, primary_color, subtitle, phone, logo_file, api_key, cors_origins, enabled_modules, allowed_ips) VALUES (:id, :nimi, :luotu, :aktiivinen, :primary_color, :subtitle, :phone, :logo_file, :api_key, :cors_origins, :enabled_modules, :allowed_ips) ON DUPLICATE KEY UPDATE nimi = VALUES(nimi), aktiivinen = VALUES(aktiivinen), primary_color = VALUES(primary_color), subtitle = VALUES(subtitle), phone = VALUES(phone), logo_file = VALUES(logo_file), api_key = VALUES(api_key), cors_origins = VALUES(cors_origins), enabled_modules = VALUES(enabled_modules), allowed_ips = VALUES(allowed_ips) ", [ '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'] ?? '', 'phone' => $company['phone'] ?? '', 'logo_file' => $company['logo_file'] ?? '', 'api_key' => $company['api_key'] ?? '', 'cors_origins' => $company['cors_origins'] ?? '', 'enabled_modules' => $enabledModulesJson, 'allowed_ips' => $company['allowed_ips'] ?? '', ]); // Päivitä domainit _dbExecute("DELETE FROM company_domains WHERE company_id = ?", [$company['id']]); if (!empty($company['domains'])) { foreach ($company['domains'] as $domain) { $domain = trim($domain); if ($domain) { _dbExecute("INSERT INTO company_domains (company_id, domain) VALUES (?, ?)", [$company['id'], $domain]); } } } $db->commit(); } catch (Exception $e) { $db->rollback(); throw $e; } } function dbDeleteCompany(string $companyId): void { _dbExecute("DELETE FROM companies WHERE id = ?", [$companyId]); } function dbGetBranding(string $host): array { $host = strtolower(trim($host)); $company = _dbFetchOne(" SELECT c.* FROM companies c JOIN company_domains cd ON c.id = cd.company_id WHERE LOWER(cd.domain) = ? LIMIT 1 ", [$host]); if ($company) { $logoUrl = !empty($company['logo_file']) ? "api.php?action=company_logo&company_id=" . urlencode($company['id']) : ''; $rawModules = $company['enabled_modules'] ?? ''; $enabledModules = $rawModules ? (json_decode($rawModules, true) ?: []) : []; return [ 'found' => true, 'company_id' => $company['id'], 'nimi' => $company['nimi'], 'primary_color' => $company['primary_color'] ?? '#0f3460', 'subtitle' => $company['subtitle'] ?? '', 'logo_url' => $logoUrl, 'enabled_modules' => $enabledModules, ]; } return [ 'found' => false, 'company_id' => '', 'nimi' => 'Noxus HUB', 'primary_color' => '#0f3460', 'subtitle' => 'Hallintapaneeli', 'logo_url' => '', 'enabled_modules' => [], ]; } function dbGetCompanyByDomain(string $host): ?array { return _dbFetchOne(" SELECT c.* FROM companies c JOIN company_domains cd ON c.id = cd.company_id WHERE LOWER(cd.domain) = ? LIMIT 1 ", [strtolower(trim($host))]); } function dbGetCompanyByApiKey(string $apiKey): ?array { return _dbFetchOne("SELECT * FROM companies WHERE api_key = ? AND api_key != '' LIMIT 1", [$apiKey]); } // ==================== KÄYTTÄJÄT ==================== function dbLoadUsers(): array { $users = _dbFetchAll("SELECT * FROM users ORDER BY luotu"); foreach ($users as &$u) { $u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$u['id']]); // Yrityskohtaiset roolit $roleRows = _dbFetchAll("SELECT company_id, role FROM user_companies WHERE user_id = ?", [$u['id']]); $companyRoles = []; foreach ($roleRows as $rr) { $companyRoles[$rr['company_id']] = $rr['role'] ?? 'user'; } $u['company_roles'] = $companyRoles; $sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$u['id']]); $sigs = []; foreach ($sigRows as $row) { $sigs[$row['mailbox_id']] = $row['signature']; } $u['signatures'] = $sigs; } return $users; } function dbGetUser(string $id): ?array { $u = _dbFetchOne("SELECT * FROM users WHERE id = ?", [$id]); if (!$u) return null; $u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$id]); $roleRows = _dbFetchAll("SELECT company_id, role FROM user_companies WHERE user_id = ?", [$id]); $companyRoles = []; foreach ($roleRows as $rr) { $companyRoles[$rr['company_id']] = $rr['role'] ?? 'user'; } $u['company_roles'] = $companyRoles; $sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$id]); $sigs = []; foreach ($sigRows as $row) { $sigs[$row['mailbox_id']] = $row['signature']; } $u['signatures'] = $sigs; $u['hidden_mailboxes'] = _dbFetchColumn("SELECT mailbox_id FROM user_hidden_mailboxes WHERE user_id = ?", [$id]); return $u; } function dbGetUserByUsername(string $username): ?array { $u = _dbFetchOne("SELECT * FROM users WHERE username = ?", [$username]); if (!$u) return null; $u['companies'] = _dbFetchColumn("SELECT company_id FROM user_companies WHERE user_id = ?", [$u['id']]); $roleRows = _dbFetchAll("SELECT company_id, role FROM user_companies WHERE user_id = ?", [$u['id']]); $companyRoles = []; foreach ($roleRows as $rr) { $companyRoles[$rr['company_id']] = $rr['role'] ?? 'user'; } $u['company_roles'] = $companyRoles; $sigRows = _dbFetchAll("SELECT mailbox_id, signature FROM user_signatures WHERE user_id = ?", [$u['id']]); $sigs = []; foreach ($sigRows as $row) { $sigs[$row['mailbox_id']] = $row['signature']; } $u['signatures'] = $sigs; $u['hidden_mailboxes'] = _dbFetchColumn("SELECT mailbox_id FROM user_hidden_mailboxes WHERE user_id = ?", [$u['id']]); return $u; } function dbSaveUser(array $user): void { $db = getDb(); $db->begin_transaction(); try { _dbExecute(" 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) ", [ '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 + yrityskohtaiset roolit _dbExecute("DELETE FROM user_companies WHERE user_id = ?", [$user['id']]); if (!empty($user['companies'])) { $companyRoles = $user['company_roles'] ?? []; foreach ($user['companies'] as $cid) { $role = $companyRoles[$cid] ?? 'user'; _dbExecute("INSERT IGNORE INTO user_companies (user_id, company_id, role) VALUES (?, ?, ?)", [$user['id'], $cid, $role]); } } // Allekirjoitukset _dbExecute("DELETE FROM user_signatures WHERE user_id = ?", [$user['id']]); if (!empty($user['signatures'])) { foreach ($user['signatures'] as $mbId => $sig) { _dbExecute("INSERT INTO user_signatures (user_id, mailbox_id, signature) VALUES (?, ?, ?)", [$user['id'], $mbId, $sig]); } } // Piilotetut postilaatikot if (array_key_exists('hidden_mailboxes', $user)) { _dbExecute("DELETE FROM user_hidden_mailboxes WHERE user_id = ?", [$user['id']]); if (!empty($user['hidden_mailboxes'])) { foreach ($user['hidden_mailboxes'] as $mbId) { _dbExecute("INSERT IGNORE INTO user_hidden_mailboxes (user_id, mailbox_id) VALUES (?, ?)", [$user['id'], $mbId]); } } } $db->commit(); } catch (Exception $e) { $db->rollback(); throw $e; } } function dbDeleteUser(string $userId): void { _dbExecute("DELETE FROM users WHERE id = ?", [$userId]); } function dbRemoveUserFromCompany(string $userId, string $companyId): void { _dbExecute("DELETE FROM user_companies WHERE user_id = ? AND company_id = ?", [$userId, $companyId]); } function dbGetUserCompanies(string $userId): array { return _dbFetchAll("SELECT company_id FROM user_companies WHERE user_id = ?", [$userId]); } // ==================== ASETUKSET (global) ==================== function dbLoadConfig(): array { $rows = _dbFetchAll("SELECT config_key, config_value FROM config"); $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 { foreach ($config as $key => $value) { _dbExecute( "INSERT INTO config (config_key, config_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)", [$key, is_array($value) ? json_encode($value) : (string)$value] ); } } // ==================== SALASANAN PALAUTUS ==================== function dbSaveToken(string $userId, string $token): void { _dbExecute("INSERT INTO reset_tokens (user_id, token, created_at) VALUES (?, ?, NOW())", [$userId, hash('sha256', $token)]); } function dbValidateToken(string $token): ?string { $hash = hash('sha256', $token); $row = _dbFetchOne("SELECT user_id FROM reset_tokens WHERE token = ? AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)", [$hash]); return $row ? $row['user_id'] : null; } function dbRemoveToken(string $token): void { _dbExecute("DELETE FROM reset_tokens WHERE token = ?", [hash('sha256', $token)]); // Siivoa vanhentuneet getDb()->query("DELETE FROM reset_tokens WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 HOUR)"); } // ==================== RATE LIMITING ==================== function dbCheckRateLimit(string $ip): bool { $count = _dbFetchScalar("SELECT COUNT(*) FROM login_attempts WHERE ip = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)", [$ip]); return (int)$count < 10; } function dbRecordLoginAttempt(string $ip): void { _dbExecute("INSERT INTO login_attempts (ip, attempted_at) VALUES (?, NOW())", [$ip]); // Siivoa vanhat (yli 1h) getDb()->query("DELETE FROM login_attempts WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 1 HOUR)"); } // ==================== ASIAKKAAT ==================== function dbLoadCustomers(string $companyId): array { $customers = _dbFetchAll("SELECT * FROM customers WHERE company_id = ? ORDER BY yritys", [$companyId]); foreach ($customers as &$c) { $conns = _dbFetchAll("SELECT * FROM customer_connections WHERE customer_id = ?", [$c['id']]); $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'] ?? '', 'vlan' => $conn['vlan'] ?? '', 'laite' => $conn['laite'] ?? '', 'portti' => $conn['portti'] ?? '', 'ip' => $conn['ip'] ?? '', ]; }, $conns); unset($c['company_id']); } return $customers; } function dbSaveCustomer(string $companyId, array $customer): void { $db = getDb(); $db->begin_transaction(); try { _dbExecute(" INSERT INTO customers (id, company_id, yritys, yhteyshenkilö, puhelin, sahkoposti, laskutusosoite, laskutuspostinumero, laskutuskaupunki, laskutussahkoposti, elaskuosoite, elaskuvalittaja, ytunnus, lisatiedot, priority_emails, luotu, muokattu, muokkaaja) VALUES (:id, :company_id, :yritys, :yhteyshenkilö, :puhelin, :sahkoposti, :laskutusosoite, :laskutuspostinumero, :laskutuskaupunki, :laskutussahkoposti, :elaskuosoite, :elaskuvalittaja, :ytunnus, :lisatiedot, :priority_emails, :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), priority_emails = VALUES(priority_emails), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) ", [ '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'] ?? '', 'priority_emails' => $customer['priority_emails'] ?? '', 'luotu' => $customer['luotu'] ?? date('Y-m-d H:i:s'), 'muokattu' => $customer['muokattu'] ?? null, 'muokkaaja' => $customer['muokkaaja'] ?? '', ]); // Päivitä liittymät _dbExecute("DELETE FROM customer_connections WHERE customer_id = ?", [$customer['id']]); if (!empty($customer['liittymat'])) { foreach ($customer['liittymat'] as $l) { _dbExecute(" INSERT INTO customer_connections (customer_id, asennusosoite, postinumero, kaupunki, liittymanopeus, hinta, sopimuskausi, alkupvm, vlan, laite, portti, ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ", [ $customer['id'], $l['asennusosoite'] ?? '', $l['postinumero'] ?? '', $l['kaupunki'] ?? '', $l['liittymanopeus'] ?? '', $l['hinta'] ?? 0, $l['sopimuskausi'] ?? '', $l['alkupvm'] ?? '', $l['vlan'] ?? '', $l['laite'] ?? '', $l['portti'] ?? '', $l['ip'] ?? '', ]); } } $db->commit(); } catch (Exception $e) { $db->rollback(); throw $e; } } function dbDeleteCustomer(string $customerId): void { _dbExecute("DELETE FROM customers WHERE id = ?", [$customerId]); } // ==================== SIJAINNIT (SITES) — POISTETTU, KÄYTETÄÄN LAITETILOJA ==================== // Sites on yhdistetty laitetiloihin. Migraatio kopioi vanhat sites → laitetilat. // dbLoadSites, dbSaveSite, dbDeleteSite poistettu. // ==================== LAITTEET (DEVICES) ==================== function dbLoadDevices(string $companyId): array { $devices = _dbFetchAll(" SELECT d.*, lt.nimi AS laitetila_name FROM devices d LEFT JOIN laitetilat lt ON d.laitetila_id = lt.id WHERE d.company_id = ? ORDER BY d.nimi ", [$companyId]); foreach ($devices as &$d) { $d['ping_check'] = (bool)$d['ping_check']; unset($d['company_id']); } return $devices; } function dbSaveDevice(string $companyId, array $device): void { _dbExecute(" INSERT INTO devices (id, company_id, nimi, hallintaosoite, serial, site_id, laitetila_id, funktio, tyyppi, malli, ping_check, lisatiedot, luotu, muokattu, muokkaaja) VALUES (:id, :company_id, :nimi, :hallintaosoite, :serial, :site_id, :laitetila_id, :funktio, :tyyppi, :malli, :ping_check, :lisatiedot, :luotu, :muokattu, :muokkaaja) ON DUPLICATE KEY UPDATE nimi = VALUES(nimi), hallintaosoite = VALUES(hallintaosoite), serial = VALUES(serial), site_id = VALUES(site_id), laitetila_id = VALUES(laitetila_id), funktio = VALUES(funktio), tyyppi = VALUES(tyyppi), malli = VALUES(malli), ping_check = VALUES(ping_check), lisatiedot = VALUES(lisatiedot), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) ", [ 'id' => $device['id'], 'company_id' => $companyId, 'nimi' => $device['nimi'] ?? '', 'hallintaosoite' => $device['hallintaosoite'] ?? '', 'serial' => $device['serial'] ?? '', 'site_id' => !empty($device['site_id']) ? $device['site_id'] : null, 'laitetila_id' => !empty($device['laitetila_id']) ? $device['laitetila_id'] : null, 'funktio' => $device['funktio'] ?? '', 'tyyppi' => $device['tyyppi'] ?? '', 'malli' => $device['malli'] ?? '', 'ping_check' => $device['ping_check'] ?? false, 'lisatiedot' => $device['lisatiedot'] ?? '', 'luotu' => $device['luotu'] ?? date('Y-m-d H:i:s'), 'muokattu' => $device['muokattu'] ?? null, 'muokkaaja' => $device['muokkaaja'] ?? '', ]); } function dbDeleteDevice(string $deviceId): void { _dbExecute("DELETE FROM devices WHERE id = ?", [$deviceId]); } // ==================== IPAM ==================== function dbLoadIpam(string $companyId): array { $rows = _dbFetchAll(" SELECT i.*, lt.nimi AS site_name FROM ipam i LEFT JOIN laitetilat lt ON i.site_id = lt.id WHERE i.company_id = ? ORDER BY i.tyyppi, i.vlan_id, i.verkko ", [$companyId]); foreach ($rows as &$r) { unset($r['company_id']); } return $rows; } function dbSaveIpam(string $companyId, array $entry): void { _dbExecute(" INSERT INTO ipam (id, company_id, tyyppi, nimi, verkko, vlan_id, site_id, tila, asiakas, lisatiedot, luotu, muokattu, muokkaaja) VALUES (:id, :company_id, :tyyppi, :nimi, :verkko, :vlan_id, :site_id, :tila, :asiakas, :lisatiedot, :luotu, :muokattu, :muokkaaja) ON DUPLICATE KEY UPDATE tyyppi = VALUES(tyyppi), nimi = VALUES(nimi), verkko = VALUES(verkko), vlan_id = VALUES(vlan_id), site_id = VALUES(site_id), tila = VALUES(tila), asiakas = VALUES(asiakas), lisatiedot = VALUES(lisatiedot), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) ", [ 'id' => $entry['id'], 'company_id' => $companyId, 'tyyppi' => $entry['tyyppi'] ?? 'ip', 'nimi' => $entry['nimi'] ?? '', 'verkko' => $entry['verkko'] ?? '', 'vlan_id' => !empty($entry['vlan_id']) ? (int)$entry['vlan_id'] : null, 'site_id' => !empty($entry['site_id']) ? $entry['site_id'] : null, 'tila' => $entry['tila'] ?? 'vapaa', 'asiakas' => $entry['asiakas'] ?? '', 'lisatiedot' => $entry['lisatiedot'] ?? '', 'luotu' => $entry['luotu'] ?? date('Y-m-d H:i:s'), 'muokattu' => $entry['muokattu'] ?? null, 'muokkaaja' => $entry['muokkaaja'] ?? '', ]); } function dbDeleteIpam(string $id): void { _dbExecute("DELETE FROM ipam WHERE id = ?", [$id]); } // ==================== OHJEET (GUIDES) ==================== function dbLoadGuideCategories(string $companyId): array { return _dbFetchAll("SELECT * FROM guide_categories WHERE company_id = ? ORDER BY sort_order, nimi", [$companyId]); } function dbSaveGuideCategory(string $companyId, array $cat): void { _dbExecute(" INSERT INTO guide_categories (id, company_id, nimi, sort_order) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE nimi = VALUES(nimi), sort_order = VALUES(sort_order) ", [$cat['id'], $companyId, $cat['nimi'] ?? '', $cat['sort_order'] ?? 0]); } function dbDeleteGuideCategory(string $catId): void { _dbExecute("DELETE FROM guide_categories WHERE id = ?", [$catId]); } function dbLoadGuides(string $companyId): array { $rows = _dbFetchAll(" SELECT g.*, gc.nimi AS category_name FROM guides g LEFT JOIN guide_categories gc ON g.category_id = gc.id WHERE g.company_id = ? ORDER BY g.pinned DESC, g.muokattu DESC, g.luotu DESC ", [$companyId]); foreach ($rows as &$r) { $r['pinned'] = (bool)$r['pinned']; } return $rows; } function dbLoadGuide(string $guideId): ?array { $rows = _dbFetchAll(" SELECT g.*, gc.nimi AS category_name FROM guides g LEFT JOIN guide_categories gc ON g.category_id = gc.id WHERE g.id = ? ", [$guideId]); if (empty($rows)) return null; $r = $rows[0]; $r['pinned'] = (bool)$r['pinned']; return $r; } function dbSaveGuide(string $companyId, array $g): void { _dbExecute(" INSERT INTO guides (id, company_id, category_id, title, content, tags, author, pinned, luotu, muokattu, muokkaaja) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE category_id = VALUES(category_id), title = VALUES(title), content = VALUES(content), tags = VALUES(tags), pinned = VALUES(pinned), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) ", [ $g['id'], $companyId, !empty($g['category_id']) ? $g['category_id'] : null, $g['title'] ?? '', $g['content'] ?? '', $g['tags'] ?? '', $g['author'] ?? '', $g['pinned'] ? 1 : 0, $g['luotu'] ?? date('Y-m-d H:i:s'), $g['muokattu'] ?? null, $g['muokkaaja'] ?? '' ]); } function dbDeleteGuide(string $guideId): void { _dbExecute("DELETE FROM guides WHERE id = ?", [$guideId]); } // ==================== LIIDIT ==================== function dbLoadLeads(string $companyId): array { $leads = _dbFetchAll("SELECT * FROM leads WHERE company_id = ? ORDER BY luotu DESC", [$companyId]); foreach ($leads as &$l) { unset($l['company_id']); } return $leads; } function dbSaveLead(string $companyId, array $lead): void { _dbExecute(" 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) ", [ '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 { _dbExecute("DELETE FROM leads WHERE id = ?", [$leadId]); } // ==================== TIKETIT ==================== /** * Generoi seuraava tikettinumero (VVNKKNN-formaatti). * Vuosi+kuukausi sekoitetaan juoksevaan numeroon. */ function dbNextTicketNumber(string $companyId): int { $yy = (int)date('y'); $mm = (int)date('m'); $fullYear = (int)date('Y'); $count = (int)_dbFetchScalar( "SELECT COUNT(*) FROM tickets WHERE company_id = ? AND YEAR(created) = ? AND MONTH(created) = ?", [$companyId, $fullYear, $mm] ); $seq = $count + 1; $hundreds = intdiv($seq, 100); $remainder = $seq % 100; return $yy * 100000 + $hundreds * 10000 + $mm * 100 + $remainder; } function dbLoadTickets(string $companyId): array { $tickets = _dbFetchAll("SELECT * FROM tickets WHERE company_id = ? ORDER BY updated DESC", [$companyId]); foreach ($tickets as &$t) { // Viestit $msgs = _dbFetchAll("SELECT * FROM ticket_messages WHERE ticket_id = ? ORDER BY timestamp", [$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'] ?? '', ]; }, $msgs); // Tagit $t['tags'] = _dbFetchColumn("SELECT tag FROM ticket_tags WHERE ticket_id = ?", [$t['id']]); unset($t['company_id']); } return $tickets; } function dbSaveTicket(string $companyId, array $ticket): void { $db = getDb(); $db->begin_transaction(); try { _dbExecute(" INSERT INTO tickets (id, ticket_number, company_id, subject, from_email, from_name, status, type, assigned_to, customer_id, customer_name, message_id, mailbox_id, cc, priority, auto_close_at, created, updated) VALUES (:id, :ticket_number, :company_id, :subject, :from_email, :from_name, :status, :type, :assigned_to, :customer_id, :customer_name, :message_id, :mailbox_id, :cc, :priority, :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), cc = VALUES(cc), priority = VALUES(priority), auto_close_at = VALUES(auto_close_at), updated = VALUES(updated) ", [ 'id' => $ticket['id'], 'ticket_number' => $ticket['ticket_number'] ?? null, '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'] ?? '', 'cc' => $ticket['cc'] ?? '', 'priority' => $ticket['priority'] ?? 'normaali', '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'])) { foreach ($ticket['messages'] as $m) { _dbExecute(" INSERT IGNORE INTO ticket_messages (id, ticket_id, type, from_email, from_name, body, timestamp, message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ", [ $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 _dbExecute("DELETE FROM ticket_tags WHERE ticket_id = ?", [$ticket['id']]); if (!empty($ticket['tags'])) { foreach ($ticket['tags'] as $tag) { if ($tag) { _dbExecute("INSERT INTO ticket_tags (ticket_id, tag) VALUES (?, ?)", [$ticket['id'], $tag]); } } } $db->commit(); } catch (Exception $e) { $db->rollback(); throw $e; } } function dbDeleteTicket(string $ticketId): void { _dbExecute("DELETE FROM tickets WHERE id = ?", [$ticketId]); } function dbFindTicketByMessageId(string $companyId, string $messageId): ?array { // Ensin tikettitasolla $t = _dbFetchOne("SELECT * FROM tickets WHERE company_id = ? AND message_id = ? LIMIT 1", [$companyId, $messageId]); if ($t) return $t; // Sitten viestien message_id:stä return _dbFetchOne(" 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 ", [$companyId, $messageId]); } // ==================== ARKISTO ==================== function dbLoadArchive(string $companyId): array { $rows = _dbFetchAll("SELECT * FROM archives WHERE company_id = ? ORDER BY archived_at DESC", [$companyId]); 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 { _dbExecute("INSERT INTO archives (id, company_id, data, archived_at) VALUES (?, ?, ?, NOW())", [$customerData['id'], $companyId, json_encode($customerData, JSON_UNESCAPED_UNICODE)]); } function dbRestoreArchive(string $archiveId): ?array { $row = _dbFetchOne("SELECT * FROM archives WHERE id = ?", [$archiveId]); if (!$row) return null; _dbExecute("DELETE FROM archives WHERE id = ?", [$archiveId]); return json_decode($row['data'], true); } function dbDeleteArchive(string $archiveId): void { _dbExecute("DELETE FROM archives WHERE id = ?", [$archiveId]); } // ==================== CHANGELOG ==================== function dbAddLog(string $companyId, string $user, string $action, string $customerId = '', string $customerName = '', string $details = ''): void { if (empty($companyId)) return; $id = bin2hex(random_bytes(8)); _dbExecute(" INSERT INTO changelog (id, company_id, timestamp, user, action, customer_id, customer_name, details) VALUES (?, ?, NOW(), ?, ?, ?, ?, ?) ", [$id, $companyId, $user, $action, $customerId, $customerName, $details]); // Pidä max 500 per yritys getDb()->query("DELETE FROM changelog WHERE company_id = '" . getDb()->real_escape_string($companyId) . "' AND id NOT IN ( SELECT id FROM (SELECT id FROM changelog WHERE company_id = '" . getDb()->real_escape_string($companyId) . "' ORDER BY timestamp DESC LIMIT 500) tmp )"); } function dbLoadChangelog(string $companyId, int $limit = 100): array { return _dbFetchAll("SELECT * FROM changelog WHERE company_id = ? ORDER BY timestamp DESC LIMIT ?", [$companyId, $limit]); } // ==================== POSTILAATIKOT ==================== function dbLoadMailboxes(string $companyId): array { $boxes = _dbFetchAll("SELECT * FROM mailboxes WHERE company_id = ?", [$companyId]); foreach ($boxes as &$b) { $b['aktiivinen'] = (bool)$b['aktiivinen']; $b['auto_reply_enabled'] = (bool)($b['auto_reply_enabled'] ?? false); $b['auto_reply_body'] = $b['auto_reply_body'] ?? ''; $b['imap_port'] = (int)$b['imap_port']; $b['smtp_port'] = (int)($b['smtp_port'] ?? 587); unset($b['company_id']); } return $boxes; } function dbSaveMailbox(string $companyId, array $mailbox): void { _dbExecute(" INSERT INTO mailboxes (id, company_id, nimi, imap_host, imap_port, imap_user, imap_encryption, imap_password, smtp_from_email, smtp_from_name, smtp_host, smtp_port, smtp_user, smtp_password, smtp_encryption, aktiivinen, auto_reply_enabled, auto_reply_body) VALUES (:id, :company_id, :nimi, :imap_host, :imap_port, :imap_user, :imap_encryption, :imap_password, :smtp_from_email, :smtp_from_name, :smtp_host, :smtp_port, :smtp_user, :smtp_password, :smtp_encryption, :aktiivinen, :auto_reply_enabled, :auto_reply_body) 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), smtp_host = VALUES(smtp_host), smtp_port = VALUES(smtp_port), smtp_user = VALUES(smtp_user), smtp_password = VALUES(smtp_password), smtp_encryption = VALUES(smtp_encryption), aktiivinen = VALUES(aktiivinen), auto_reply_enabled = VALUES(auto_reply_enabled), auto_reply_body = VALUES(auto_reply_body) ", [ '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'] ?? '', 'smtp_host' => $mailbox['smtp_host'] ?? '', 'smtp_port' => $mailbox['smtp_port'] ?? 587, 'smtp_user' => $mailbox['smtp_user'] ?? '', 'smtp_password' => $mailbox['smtp_password'] ?? '', 'smtp_encryption' => $mailbox['smtp_encryption'] ?? 'tls', 'aktiivinen' => $mailbox['aktiivinen'] ?? true, 'auto_reply_enabled' => $mailbox['auto_reply_enabled'] ?? false, 'auto_reply_body' => $mailbox['auto_reply_body'] ?? '', ]); } function dbDeleteMailbox(string $mailboxId): void { _dbExecute("DELETE FROM mailboxes WHERE id = ?", [$mailboxId]); } function dbGetMailbox(string $mailboxId): ?array { $b = _dbFetchOne("SELECT * FROM mailboxes WHERE id = ?", [$mailboxId]); if ($b) { $b['aktiivinen'] = (bool)$b['aktiivinen']; $b['imap_port'] = (int)$b['imap_port']; } return $b; } // ==================== TIKETTISÄÄNNÖT ==================== function dbLoadTicketRules(string $companyId): array { $rules = _dbFetchAll("SELECT * FROM ticket_rules WHERE company_id = ? ORDER BY priority", [$companyId]); foreach ($rules as &$r) { $r['priority'] = (int)$r['priority']; $r['auto_close_days'] = (int)$r['auto_close_days']; $r['enabled'] = !empty($r['enabled']); unset($r['company_id']); } return $rules; } function dbSaveTicketRule(string $companyId, array $rule): void { _dbExecute(" INSERT INTO ticket_rules (id, company_id, name, from_contains, subject_contains, to_contains, priority, tag, assign_to, status_set, type_set, set_priority, set_tags, auto_close_days, enabled) VALUES (:id, :company_id, :name, :from_contains, :subject_contains, :to_contains, :priority, :tag, :assign_to, :status_set, :type_set, :set_priority, :set_tags, :auto_close_days, :enabled) ON DUPLICATE KEY UPDATE name = VALUES(name), from_contains = VALUES(from_contains), subject_contains = VALUES(subject_contains), to_contains = VALUES(to_contains), priority = VALUES(priority), tag = VALUES(tag), assign_to = VALUES(assign_to), status_set = VALUES(status_set), type_set = VALUES(type_set), set_priority = VALUES(set_priority), set_tags = VALUES(set_tags), auto_close_days = VALUES(auto_close_days), enabled = VALUES(enabled) ", [ 'id' => $rule['id'], 'company_id' => $companyId, 'name' => $rule['name'] ?? '', 'from_contains' => $rule['from_contains'] ?? '', 'subject_contains' => $rule['subject_contains'] ?? '', 'to_contains' => $rule['to_contains'] ?? '', 'priority' => $rule['priority'] ?? 0, 'tag' => $rule['tag'] ?? '', 'assign_to' => $rule['assign_to'] ?? '', 'status_set' => $rule['set_status'] ?? $rule['status_set'] ?? '', 'type_set' => $rule['set_type'] ?? $rule['type_set'] ?? '', 'set_priority' => $rule['set_priority'] ?? '', 'set_tags' => $rule['set_tags'] ?? '', 'auto_close_days' => $rule['auto_close_days'] ?? 0, 'enabled' => !empty($rule['enabled']) ? 1 : 0, ]); } function dbDeleteTicketRule(string $ruleId): void { _dbExecute("DELETE FROM ticket_rules WHERE id = ?", [$ruleId]); } // ==================== TIKETTITYYPIT ==================== function dbLoadTicketTypes(string $companyId): array { $types = _dbFetchAll("SELECT * FROM ticket_types WHERE company_id = ? ORDER BY sort_order, label", [$companyId]); // Jos ei tyyppejä, luo oletukset if (empty($types)) { $defaults = [ ['value' => 'laskutus', 'label' => 'Laskutus', 'sort_order' => 1], ['value' => 'tekniikka', 'label' => 'Tekniikka', 'sort_order' => 2], ['value' => 'vika', 'label' => 'Vika', 'sort_order' => 3], ['value' => 'abuse', 'label' => 'Abuse', 'sort_order' => 4], ['value' => 'muu', 'label' => 'Muu', 'sort_order' => 5], ]; foreach ($defaults as $d) { dbSaveTicketType($companyId, $d); } $types = _dbFetchAll("SELECT * FROM ticket_types WHERE company_id = ? ORDER BY sort_order, label", [$companyId]); } foreach ($types as &$t) { $t['sort_order'] = (int)($t['sort_order'] ?? 0); unset($t['company_id']); } return $types; } function dbSaveTicketType(string $companyId, array $type): void { $id = $type['id'] ?? generateId(); _dbExecute(" INSERT INTO ticket_types (id, company_id, value, label, color, sort_order) VALUES (:id, :company_id, :value, :label, :color, :sort_order) ON DUPLICATE KEY UPDATE label = VALUES(label), color = VALUES(color), sort_order = VALUES(sort_order) ", [ 'id' => $id, 'company_id' => $companyId, 'value' => $type['value'] ?? '', 'label' => $type['label'] ?? '', 'color' => $type['color'] ?? '', 'sort_order' => $type['sort_order'] ?? 0, ]); } function dbDeleteTicketType(string $companyId, string $value): void { _dbExecute("DELETE FROM ticket_types WHERE company_id = ? AND value = ?", [$companyId, $value]); } // ==================== HAETUT MESSAGE-ID:T (duplikaattien esto) ==================== function dbGetFetchedMessageIds(string $companyId): array { $rows = _dbFetchAll("SELECT message_id FROM fetched_message_ids WHERE company_id = ?", [$companyId]); $ids = []; foreach ($rows as $r) { $ids[$r['message_id']] = true; } return $ids; } function dbMarkMessageIdFetched(string $companyId, string $messageId): void { if (empty($messageId)) return; try { _dbExecute("INSERT IGNORE INTO fetched_message_ids (company_id, message_id) VALUES (?, ?)", [$companyId, $messageId]); } catch (\Throwable $e) { /* duplicate, ok */ } } // ==================== 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 { return _dbFetchScalar("SELECT api_key FROM companies WHERE id = ?", [$companyId]) ?: ''; } function dbGetCompanyCorsOrigins(string $companyId): array { $val = _dbFetchScalar("SELECT cors_origins FROM companies WHERE id = ?", [$companyId]); if (!$val) return []; $decoded = json_decode($val, true); return is_array($decoded) ? $decoded : []; } function dbSetCompanyApiKey(string $companyId, string $apiKey): void { _dbExecute("UPDATE companies SET api_key = ? WHERE id = ?", [$apiKey, $companyId]); } function dbSetCompanyCorsOrigins(string $companyId, array $origins): void { _dbExecute("UPDATE companies SET cors_origins = ? WHERE id = ?", [json_encode($origins), $companyId]); } // ==================== VASTAUSPOHJAT ==================== function dbLoadTemplates(string $companyId): array { $templates = _dbFetchAll("SELECT * FROM reply_templates WHERE company_id = ? ORDER BY sort_order, nimi", [$companyId]); foreach ($templates as &$t) { $t['sort_order'] = (int)$t['sort_order']; unset($t['company_id']); } return $templates; } function dbSaveTemplate(string $companyId, array $tpl): void { _dbExecute(" INSERT INTO reply_templates (id, company_id, nimi, body, sort_order) VALUES (:id, :company_id, :nimi, :body, :sort_order) ON DUPLICATE KEY UPDATE nimi = VALUES(nimi), body = VALUES(body), sort_order = VALUES(sort_order) ", [ 'id' => $tpl['id'], 'company_id' => $companyId, 'nimi' => $tpl['nimi'] ?? '', 'body' => $tpl['body'] ?? '', 'sort_order' => $tpl['sort_order'] ?? 0, ]); } function dbDeleteTemplate(string $templateId): void { _dbExecute("DELETE FROM reply_templates WHERE id = ?", [$templateId]); } // ==================== PRIORITY EMAILS (ASIAKKUUDET) ==================== function dbIsPriorityEmail(string $companyId, string $email): bool { $email = strtolower(trim($email)); if (!$email) return false; // Hae kaikki asiakkaiden priority_emails kentät ja tarkista onko sähköposti listalla $rows = _dbFetchAll("SELECT priority_emails FROM customers WHERE company_id = ? AND priority_emails != ''", [$companyId]); foreach ($rows as $row) { $emails = array_map('strtolower', array_map('trim', explode("\n", $row['priority_emails']))); if (in_array($email, $emails)) return true; } return false; } // ==================== TEHTÄVÄT (TODOS) ==================== function dbLoadTodos(string $companyId): array { $rows = _dbFetchAll(" SELECT t.*, COALESCE((SELECT SUM(te.hours) FROM todo_time_entries te WHERE te.todo_id = t.id), 0) AS total_hours, (SELECT COUNT(*) FROM todo_comments tc WHERE tc.todo_id = t.id) AS comment_count, (SELECT COUNT(*) FROM todo_subtasks ts WHERE ts.todo_id = t.id) AS subtask_count, (SELECT COUNT(*) FROM todo_subtasks ts2 WHERE ts2.todo_id = t.id AND ts2.completed = 1) AS subtask_done FROM todos t WHERE t.company_id = ? ORDER BY CASE t.priority WHEN 'kiireellinen' THEN 0 WHEN 'tarkea' THEN 1 ELSE 2 END, t.deadline IS NULL, t.deadline ASC, t.luotu DESC ", [$companyId]); foreach ($rows as &$r) { $r['total_hours'] = floatval($r['total_hours']); $r['comment_count'] = intval($r['comment_count']); $r['subtask_count'] = intval($r['subtask_count']); $r['subtask_done'] = intval($r['subtask_done']); unset($r['company_id']); } return $rows; } function dbLoadTodo(string $todoId): ?array { $row = _dbFetchAll("SELECT * FROM todos WHERE id = ?", [$todoId]); if (empty($row)) return null; $todo = $row[0]; $todo['comments'] = _dbFetchAll("SELECT * FROM todo_comments WHERE todo_id = ? ORDER BY luotu", [$todoId]); $todo['time_entries'] = _dbFetchAll("SELECT * FROM todo_time_entries WHERE todo_id = ? ORDER BY work_date DESC, luotu DESC", [$todoId]); $todo['subtasks'] = _dbFetchAll("SELECT * FROM todo_subtasks WHERE todo_id = ? ORDER BY sort_order, luotu", [$todoId]); foreach ($todo['time_entries'] as &$te) { $te['hours'] = floatval($te['hours']); } foreach ($todo['subtasks'] as &$st) { $st['completed'] = (bool)$st['completed']; } $todo['total_hours'] = array_sum(array_column($todo['time_entries'], 'hours')); return $todo; } function dbSaveTodo(string $companyId, array $todo): void { _dbExecute(" INSERT INTO todos (id, company_id, type, title, description, status, priority, category, assigned_to, created_by, deadline, luotu, muokattu, muokkaaja) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title = VALUES(title), description = VALUES(description), status = VALUES(status), priority = VALUES(priority), category = VALUES(category), assigned_to = VALUES(assigned_to), deadline = VALUES(deadline), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) ", [ $todo['id'], $companyId, $todo['type'] ?? 'task', $todo['title'] ?? '', $todo['description'] ?? '', $todo['status'] ?? 'avoin', $todo['priority'] ?? 'normaali', $todo['category'] ?? '', $todo['assigned_to'] ?? '', $todo['created_by'] ?? '', !empty($todo['deadline']) ? $todo['deadline'] : null, $todo['luotu'] ?? date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), $todo['muokkaaja'] ?? '' ]); } function dbDeleteTodo(string $todoId): void { _dbExecute("DELETE FROM todos WHERE id = ?", [$todoId]); } function dbAddTodoComment(string $todoId, array $comment): void { _dbExecute("INSERT INTO todo_comments (id, todo_id, author, body, luotu) VALUES (?, ?, ?, ?, ?)", [ $comment['id'] ?? generateId(), $todoId, $comment['author'] ?? '', $comment['body'] ?? '', $comment['luotu'] ?? date('Y-m-d H:i:s') ]); } function dbDeleteTodoComment(string $commentId): void { _dbExecute("DELETE FROM todo_comments WHERE id = ?", [$commentId]); } function dbAddTodoTimeEntry(string $todoId, array $entry): void { _dbExecute("INSERT INTO todo_time_entries (id, todo_id, user, hours, description, work_date, luotu) VALUES (?, ?, ?, ?, ?, ?, ?)", [ $entry['id'] ?? generateId(), $todoId, $entry['user'] ?? '', $entry['hours'] ?? 0, $entry['description'] ?? '', $entry['work_date'] ?? date('Y-m-d'), $entry['luotu'] ?? date('Y-m-d H:i:s') ]); } function dbDeleteTodoTimeEntry(string $entryId): void { _dbExecute("DELETE FROM todo_time_entries WHERE id = ?", [$entryId]); } function dbAddTodoSubtask(string $todoId, array $subtask): void { $maxOrder = _dbFetchAll("SELECT COALESCE(MAX(sort_order), 0) + 1 AS next_order FROM todo_subtasks WHERE todo_id = ?", [$todoId]); _dbExecute("INSERT INTO todo_subtasks (id, todo_id, title, completed, sort_order, created_by, luotu) VALUES (?, ?, ?, 0, ?, ?, ?)", [ $subtask['id'] ?? generateId(), $todoId, $subtask['title'] ?? '', $maxOrder[0]['next_order'] ?? 0, $subtask['created_by'] ?? '', date('Y-m-d H:i:s') ]); } function dbToggleTodoSubtask(string $subtaskId): bool { _dbExecute("UPDATE todo_subtasks SET completed = NOT completed WHERE id = ?", [$subtaskId]); $row = _dbFetchAll("SELECT completed FROM todo_subtasks WHERE id = ?", [$subtaskId]); return !empty($row) && $row[0]['completed']; } function dbDeleteTodoSubtask(string $subtaskId): void { _dbExecute("DELETE FROM todo_subtasks WHERE id = ?", [$subtaskId]); } // ==================== DOKUMENTTIKANSIOT ==================== function dbLoadFolders(string $companyId, ?string $customerId = null): array { if ($customerId) { return _dbFetchAll("SELECT * FROM document_folders WHERE company_id = ? AND customer_id = ? ORDER BY name", [$companyId, $customerId]); } return _dbFetchAll("SELECT * FROM document_folders WHERE company_id = ? ORDER BY name", [$companyId]); } function dbSaveFolder(string $companyId, array $folder): string { $id = $folder['id'] ?? generateId(); $now = date('Y-m-d H:i:s'); _dbExecute(" INSERT INTO document_folders (id, company_id, customer_id, name, parent_id, created_by, luotu) VALUES (:id, :companyId, :customerId, :name, :parentId, :createdBy, :luotu) ON DUPLICATE KEY UPDATE name = VALUES(name), parent_id = VALUES(parent_id) ", [ 'id' => $id, 'companyId' => $companyId, 'customerId' => !empty($folder['customer_id']) ? $folder['customer_id'] : null, 'name' => $folder['name'] ?? '', 'parentId' => !empty($folder['parent_id']) ? $folder['parent_id'] : null, 'createdBy' => $folder['created_by'] ?? '', 'luotu' => $folder['luotu'] ?? $now ]); return $id; } function dbDeleteFolder(string $companyId, string $folderId): bool { $folder = _dbFetchOne("SELECT parent_id FROM document_folders WHERE id = ? AND company_id = ?", [$folderId, $companyId]); if (!$folder) return false; // Siirrä kansion dokumentit ylätasolle _dbExecute("UPDATE documents SET folder_id = ? WHERE folder_id = ? AND company_id = ?", [$folder['parent_id'], $folderId, $companyId]); // Siirrä alikansiot ylätasolle _dbExecute("UPDATE document_folders SET parent_id = ? WHERE parent_id = ? AND company_id = ?", [$folder['parent_id'], $folderId, $companyId]); _dbExecute("DELETE FROM document_folders WHERE id = ? AND company_id = ?", [$folderId, $companyId]); return true; } // ==================== DOKUMENTIT ==================== function dbLoadDocuments(string $companyId, ?string $customerId = null): array { $sql = "SELECT d.*, dv.original_name AS current_file, dv.file_size AS current_size, dv.created_by AS version_author, dv.luotu AS version_date FROM documents d LEFT JOIN document_versions dv ON dv.document_id = d.id AND dv.version_number = d.current_version WHERE d.company_id = :companyId"; $params = ['companyId' => $companyId]; if ($customerId !== null) { $sql .= " AND d.customer_id = :customerId"; $params['customerId'] = $customerId; } $sql .= " ORDER BY d.muokattu DESC, d.luotu DESC"; return _dbFetchAll($sql, $params); } function dbLoadDocument(string $documentId): ?array { $doc = _dbFetchOne("SELECT * FROM documents WHERE id = ?", [$documentId]); if (!$doc) return null; $doc['versions'] = _dbFetchAll( "SELECT * FROM document_versions WHERE document_id = ? ORDER BY version_number DESC", [$documentId] ); return $doc; } function dbSaveDocument(string $companyId, array $doc): string { $id = $doc['id'] ?? generateId(); $now = date('Y-m-d H:i:s'); _dbExecute(" INSERT INTO documents (id, company_id, customer_id, folder_id, title, description, category, current_version, max_versions, created_by, luotu, muokattu, muokkaaja) VALUES (:id, :companyId, :customerId, :folderId, :title, :description, :category, :currentVersion, :maxVersions, :createdBy, :luotu, :muokattu, :muokkaaja) ON DUPLICATE KEY UPDATE title = VALUES(title), description = VALUES(description), category = VALUES(category), customer_id = VALUES(customer_id), folder_id = VALUES(folder_id), max_versions = VALUES(max_versions), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) ", [ 'id' => $id, 'companyId' => $companyId, 'customerId' => !empty($doc['customer_id']) ? $doc['customer_id'] : null, 'folderId' => !empty($doc['folder_id']) ? $doc['folder_id'] : null, 'title' => $doc['title'] ?? '', 'description' => $doc['description'] ?? '', 'category' => $doc['category'] ?? 'muu', 'currentVersion' => (int)($doc['current_version'] ?? 0), 'maxVersions' => (int)($doc['max_versions'] ?? 10), 'createdBy' => $doc['created_by'] ?? '', 'luotu' => $doc['luotu'] ?? $now, 'muokattu' => $now, 'muokkaaja' => $doc['muokkaaja'] ?? '' ]); return $id; } function dbDeleteDocument(string $documentId): ?array { // Palauta dokumentin tiedot tiedostojen poistoa varten $doc = _dbFetchOne("SELECT id, company_id FROM documents WHERE id = ?", [$documentId]); if ($doc) { _dbExecute("DELETE FROM documents WHERE id = ?", [$documentId]); // CASCADE poistaa versiot } return $doc; } function dbAddDocumentVersion(string $documentId, array $version): void { $now = date('Y-m-d H:i:s'); // Hae seuraava versionumero $maxVersion = _dbFetchScalar("SELECT COALESCE(MAX(version_number), 0) FROM document_versions WHERE document_id = ?", [$documentId]); $nextVersion = (int)$maxVersion + 1; _dbExecute("INSERT INTO document_versions (id, document_id, version_number, filename, original_name, file_size, mime_type, content, change_notes, created_by, luotu) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ $version['id'] ?? generateId(), $documentId, $nextVersion, $version['filename'] ?? '', $version['original_name'] ?? '', $version['file_size'] ?? 0, $version['mime_type'] ?? '', $version['content'] ?? null, $version['change_notes'] ?? '', $version['created_by'] ?? '', $now ]); // Päivitä dokumentin current_version _dbExecute("UPDATE documents SET current_version = ?, muokattu = ?, muokkaaja = ? WHERE id = ?", [ $nextVersion, $now, $version['created_by'] ?? '', $documentId ]); // Versioiden pruning: poista vanhimmat jos yli max_versions _pruneDocumentVersions($documentId); } function _pruneDocumentVersions(string $documentId): void { $doc = _dbFetchOne("SELECT max_versions, company_id FROM documents WHERE id = ?", [$documentId]); if (!$doc) return; $maxVersions = (int)($doc['max_versions'] ?? 10); if ($maxVersions <= 0) return; // 0 = rajaton $versions = _dbFetchAll( "SELECT id, version_number, filename FROM document_versions WHERE document_id = ? ORDER BY version_number DESC", [$documentId] ); if (count($versions) <= $maxVersions) return; // Poista vanhimmat versiot (säilytä uusimmat $maxVersions kpl) $toDelete = array_slice($versions, $maxVersions); foreach ($toDelete as $v) { // Poista tiedosto levyltä jos olemassa if (!empty($v['filename'])) { $filePath = DATA_DIR . '/companies/' . $doc['company_id'] . '/documents/' . $documentId . '/' . $v['filename']; if (is_file($filePath)) unlink($filePath); } _dbExecute("DELETE FROM document_versions WHERE id = ?", [$v['id']]); } } function dbRestoreDocumentVersion(string $documentId, string $versionId, string $user): ?int { // Hae palautettava versio $oldVersion = _dbFetchOne("SELECT * FROM document_versions WHERE id = ? AND document_id = ?", [$versionId, $documentId]); if (!$oldVersion) return null; // Lisää uutena versiona (kopioi tiedostotiedot) $now = date('Y-m-d H:i:s'); $maxVersion = _dbFetchScalar("SELECT COALESCE(MAX(version_number), 0) FROM document_versions WHERE document_id = ?", [$documentId]); $nextVersion = (int)$maxVersion + 1; $newId = generateId(); _dbExecute("INSERT INTO document_versions (id, document_id, version_number, filename, original_name, file_size, mime_type, content, change_notes, created_by, luotu) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ $newId, $documentId, $nextVersion, $oldVersion['filename'], $oldVersion['original_name'], $oldVersion['file_size'], $oldVersion['mime_type'], $oldVersion['content'] ?? null, 'Palautettu versiosta ' . $oldVersion['version_number'], $user, $now ]); _dbExecute("UPDATE documents SET current_version = ?, muokattu = ?, muokkaaja = ? WHERE id = ?", [ $nextVersion, $now, $user, $documentId ]); return $nextVersion; } function dbGetDocumentVersion(string $documentId, int $versionNumber): ?array { return _dbFetchOne("SELECT * FROM document_versions WHERE document_id = ? AND version_number = ?", [$documentId, $versionNumber]); } // ==================== LAITETILAT ==================== function dbLoadLaitetilat(string $companyId): array { $tilat = _dbFetchAll("SELECT * FROM laitetilat WHERE company_id = ? ORDER BY nimi", [$companyId]); foreach ($tilat as &$t) { $t['file_count'] = (int)_dbFetchScalar("SELECT COUNT(*) FROM laitetila_files WHERE laitetila_id = ?", [$t['id']]); $t['device_count'] = (int)_dbFetchScalar("SELECT COUNT(*) FROM devices WHERE laitetila_id = ? AND company_id = ?", [$t['id'], $companyId]); $t['devices'] = _dbFetchAll("SELECT id, nimi, tyyppi, malli, hallintaosoite, ping_status FROM devices WHERE laitetila_id = ? AND company_id = ? ORDER BY nimi", [$t['id'], $companyId]); } return $tilat; } function dbLoadLaitetila(string $laitetilaId): ?array { $tila = _dbFetchOne("SELECT * FROM laitetilat WHERE id = ?", [$laitetilaId]); if (!$tila) return null; $tila['files'] = _dbFetchAll("SELECT * FROM laitetila_files WHERE laitetila_id = ? ORDER BY luotu DESC", [$laitetilaId]); return $tila; } function dbSaveLaitetila(string $companyId, array $tila): string { $id = $tila['id'] ?? generateId(); $now = date('Y-m-d H:i:s'); _dbExecute(" INSERT INTO laitetilat (id, company_id, nimi, kuvaus, osoite, luotu, muokattu, muokkaaja) VALUES (:id, :companyId, :nimi, :kuvaus, :osoite, :luotu, :muokattu, :muokkaaja) ON DUPLICATE KEY UPDATE nimi = VALUES(nimi), kuvaus = VALUES(kuvaus), osoite = VALUES(osoite), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) ", [ 'id' => $id, 'companyId' => $companyId, 'nimi' => $tila['nimi'] ?? '', 'kuvaus' => $tila['kuvaus'] ?? '', 'osoite' => $tila['osoite'] ?? '', 'luotu' => $tila['luotu'] ?? $now, 'muokattu' => $now, 'muokkaaja' => $tila['muokkaaja'] ?? '' ]); return $id; } function dbDeleteLaitetila(string $laitetilaId): ?array { $tila = _dbFetchOne("SELECT id, company_id FROM laitetilat WHERE id = ?", [$laitetilaId]); if ($tila) { // Nollaa viittaukset laitteissa ja IPAM:ssa _dbExecute("UPDATE devices SET laitetila_id = NULL WHERE laitetila_id = ?", [$laitetilaId]); _dbExecute("UPDATE devices SET site_id = NULL WHERE site_id = ?", [$laitetilaId]); _dbExecute("UPDATE ipam SET site_id = NULL WHERE site_id = ?", [$laitetilaId]); _dbExecute("DELETE FROM laitetilat WHERE id = ?", [$laitetilaId]); // CASCADE poistaa tiedostot } return $tila; } function dbAddLaitetilaFile(string $laitetilaId, array $file): void { _dbExecute("INSERT INTO laitetila_files (id, laitetila_id, filename, original_name, file_size, mime_type, description, created_by, luotu) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [ $file['id'] ?? generateId(), $laitetilaId, $file['filename'] ?? '', $file['original_name'] ?? '', $file['file_size'] ?? 0, $file['mime_type'] ?? '', $file['description'] ?? '', $file['created_by'] ?? '', date('Y-m-d H:i:s') ]); } function dbDeleteLaitetilaFile(string $fileId): ?array { $file = _dbFetchOne("SELECT lf.*, lt.company_id FROM laitetila_files lf JOIN laitetilat lt ON lt.id = lf.laitetila_id WHERE lf.id = ?", [$fileId]); if ($file) { _dbExecute("DELETE FROM laitetila_files WHERE id = ?", [$fileId]); } return $file; } // ==================== NETADMIN ==================== function dbLoadAllConnections(string $companyId): array { return _dbFetchAll(" SELECT cc.*, c.yritys AS customer_name, c.yhteyshenkilö AS customer_contact, c.puhelin AS customer_phone, c.sahkoposti AS customer_email, c.id AS customer_id, gw.nimi AS gateway_name, gw.hallintaosoite AS gateway_ip, gw.malli AS gateway_model FROM customer_connections cc JOIN customers c ON c.id = cc.customer_id LEFT JOIN devices gw ON cc.gateway_device_id = gw.id WHERE c.company_id = :companyId ORDER BY cc.kaupunki, cc.asennusosoite ", ['companyId' => $companyId]); } function dbLoadConnection(int $connectionId): ?array { return _dbFetchOne(" SELECT cc.*, c.yritys AS customer_name, c.company_id, gw.nimi AS gateway_name, gw.hallintaosoite AS gateway_ip, gw.malli AS gateway_model FROM customer_connections cc JOIN customers c ON c.id = cc.customer_id LEFT JOIN devices gw ON cc.gateway_device_id = gw.id WHERE cc.id = ? ", [$connectionId]); } function dbUpdateConnection(int $connectionId, array $data): void { _dbExecute("UPDATE customer_connections SET liittymanopeus = ?, vlan = ?, laite = ?, portti = ?, ip = ?, asennusosoite = ?, postinumero = ?, kaupunki = ?, gateway_device_id = ? WHERE id = ?", [ $data['liittymanopeus'] ?? '', $data['vlan'] ?? '', $data['laite'] ?? '', $data['portti'] ?? '', $data['ip'] ?? '', $data['asennusosoite'] ?? '', $data['postinumero'] ?? '', $data['kaupunki'] ?? '', !empty($data['gateway_device_id']) ? $data['gateway_device_id'] : null, $connectionId ]); } // ==================== INTEGRAATIOT ==================== function dbLoadIntegrations(string $companyId): array { return _dbFetchAll("SELECT * FROM integrations WHERE company_id = ? ORDER BY type", [$companyId]); } function dbGetIntegration(string $companyId, string $type): ?array { $row = _dbFetchOne("SELECT * FROM integrations WHERE company_id = ? AND type = ?", [$companyId, $type]); if ($row && $row['config']) { $row['config'] = json_decode($row['config'], true) ?: []; } return $row; } function dbSaveIntegration(string $companyId, string $type, bool $enabled, array $config): void { $existing = _dbFetchOne("SELECT id FROM integrations WHERE company_id = ? AND type = ?", [$companyId, $type]); $now = date('Y-m-d H:i:s'); if ($existing) { _dbExecute( "UPDATE integrations SET enabled = ?, config = ?, updated = ? WHERE company_id = ? AND type = ?", [$enabled ? 1 : 0, json_encode($config), $now, $companyId, $type] ); } else { $id = substr(uniqid(), -8) . bin2hex(random_bytes(2)); _dbExecute( "INSERT INTO integrations (id, company_id, type, enabled, config, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [$id, $companyId, $type, $enabled ? 1 : 0, json_encode($config), $now, $now] ); } } function dbGetTicketByZammadId(string $companyId, int $zammadId): ?array { return _dbFetchOne("SELECT * FROM tickets WHERE company_id = ? AND zammad_ticket_id = ?", [$companyId, $zammadId]); } function dbGetMessageByZammadArticleId(string $ticketId, int $articleId): ?array { return _dbFetchOne("SELECT * FROM ticket_messages WHERE ticket_id = ? AND zammad_article_id = ?", [$ticketId, $articleId]); }