Files
intra.noxus.fi/db.php
Jukka Lampikoski 8a630508f4 fix: add DEFAULT CHARSET=utf8mb4 to all tables
Foreign key constraints fail if charset doesn't match between
referencing and referenced tables. Added utf8mb4 to user_companies,
reset_tokens, and login_attempts tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:24:25 +02:00

1018 lines
41 KiB
PHP

<?php
/**
* Noxus Intra — Tietokantakerros (MySQLi)
*
* Korvaa kaikki JSON-tiedosto-operaatiot MySQL-kutsuilla.
* Funktiot palauttavat samat tietorakenteet kuin vanhat JSON-funktiot.
*/
// ==================== YHTEYS ====================
function getDb(): mysqli {
static $db = null;
if ($db === null) {
$configFile = __DIR__ . '/db_config.php';
if (!file_exists($configFile)) {
throw new RuntimeException('db_config.php puuttuu! Kopioi db_config.php.example ja täytä tiedot.');
}
$cfg = require $configFile;
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$db = new mysqli($cfg['host'], $cfg['username'], $cfg['password'], $cfg['dbname']);
$db->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+)/', 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('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 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,
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",
];
foreach ($tables as $i => $sql) {
if ($db->query($sql) === false) {
throw new RuntimeException("Taulun luonti epäonnistui (taulu #" . ($i+1) . "): " . $db->error);
}
}
}
// ==================== 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'];
}
return $companies;
}
function dbSaveCompany(array $company): void {
$db = getDb();
$db->begin_transaction();
try {
_dbExecute("
INSERT INTO companies (id, nimi, luotu, aktiivinen, primary_color, subtitle, logo_file, api_key, cors_origins)
VALUES (:id, :nimi, :luotu, :aktiivinen, :primary_color, :subtitle, :logo_file, :api_key, :cors_origins)
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)
", [
'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
_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'])
: '';
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 {
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']]);
$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]);
$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;
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']]);
$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 $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
_dbExecute("DELETE FROM user_companies WHERE user_id = ?", [$user['id']]);
if (!empty($user['companies'])) {
foreach ($user['companies'] as $cid) {
_dbExecute("INSERT IGNORE INTO user_companies (user_id, company_id) VALUES (?, ?)", [$user['id'], $cid]);
}
}
// 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]);
}
}
$db->commit();
} catch (Exception $e) {
$db->rollback();
throw $e;
}
}
function dbDeleteUser(string $userId): void {
_dbExecute("DELETE FROM users WHERE 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'] ?? '',
];
}, $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, 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)
", [
'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
_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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
", [
$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 {
_dbExecute("DELETE FROM customers WHERE id = ?", [$customerId]);
}
// ==================== 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 ====================
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, 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)
", [
'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'])) {
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['imap_port'] = (int)$b['imap_port'];
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, 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)
", [
'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 {
_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'];
unset($r['company_id']);
}
return $rules;
}
function dbSaveTicketRule(string $companyId, array $rule): void {
_dbExecute("
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)
", [
'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 {
_dbExecute("DELETE FROM ticket_rules WHERE id = ?", [$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 {
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]);
}