- Poisto-nappi näkyy dokumentin luojalle (ei enää vain admin) - API: document_delete sallii poiston adminille tai luojalle - Uusi max_versions-sarake documents-tauluun (oletus 10) - Versioiden automaattinen pruning: uuden version tallennuksen yhteydessä poistetaan vanhimmat versiot jos yli max_versions (tiedostot levyltä myös) - Valittavissa per dokumentti: 5, 10, 20, 50 tai rajaton Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2061 lines
86 KiB
PHP
2061 lines
86 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+)/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 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",
|
|
];
|
|
|
|
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",
|
|
];
|
|
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 */ }
|
|
}
|
|
|
|
// ==================== 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, logo_file, api_key, cors_origins, enabled_modules, allowed_ips)
|
|
VALUES (:id, :nimi, :luotu, :aktiivinen, :primary_color, :subtitle, :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),
|
|
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'] ?? '',
|
|
'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 Intra',
|
|
'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]);
|
|
}
|
|
|
|
// ==================== 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) ====================
|
|
|
|
function dbLoadSites(string $companyId): array {
|
|
return _dbFetchAll("SELECT * FROM sites WHERE company_id = ? ORDER BY nimi", [$companyId]);
|
|
}
|
|
|
|
function dbSaveSite(string $companyId, array $site): void {
|
|
_dbExecute("
|
|
INSERT INTO sites (id, company_id, nimi, osoite, kaupunki)
|
|
VALUES (:id, :company_id, :nimi, :osoite, :kaupunki)
|
|
ON DUPLICATE KEY UPDATE
|
|
nimi = VALUES(nimi), osoite = VALUES(osoite), kaupunki = VALUES(kaupunki)
|
|
", [
|
|
'id' => $site['id'],
|
|
'company_id' => $companyId,
|
|
'nimi' => $site['nimi'] ?? '',
|
|
'osoite' => $site['osoite'] ?? '',
|
|
'kaupunki' => $site['kaupunki'] ?? '',
|
|
]);
|
|
}
|
|
|
|
function dbDeleteSite(string $siteId): void {
|
|
// Nollaa viittaavien laitteiden site_id
|
|
_dbExecute("UPDATE devices SET site_id = NULL WHERE site_id = ?", [$siteId]);
|
|
_dbExecute("DELETE FROM sites WHERE id = ?", [$siteId]);
|
|
}
|
|
|
|
// ==================== LAITTEET (DEVICES) ====================
|
|
|
|
function dbLoadDevices(string $companyId): array {
|
|
$devices = _dbFetchAll("
|
|
SELECT d.*, s.nimi AS site_name
|
|
FROM devices d
|
|
LEFT JOIN sites s ON d.site_id = s.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, funktio, tyyppi, malli, ping_check, lisatiedot, luotu, muokattu, muokkaaja)
|
|
VALUES (:id, :company_id, :nimi, :hallintaosoite, :serial, :site_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), 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,
|
|
'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.*, s.nimi AS site_name
|
|
FROM ipam i
|
|
LEFT JOIN sites s ON i.site_id = s.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'];
|
|
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]);
|
|
}
|
|
|
|
// ==================== 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): array {
|
|
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, name, parent_id, created_by, luotu)
|
|
VALUES (:id, :companyId, :name, :parentId, :createdBy, :luotu)
|
|
ON DUPLICATE KEY UPDATE name = VALUES(name), parent_id = VALUES(parent_id)
|
|
", [
|
|
'id' => $id,
|
|
'companyId' => $companyId,
|
|
'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']]);
|
|
}
|
|
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) {
|
|
_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
|
|
FROM customer_connections cc
|
|
JOIN customers c ON c.id = cc.customer_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
|
|
FROM customer_connections cc
|
|
JOIN customers c ON c.id = cc.customer_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 = ?
|
|
WHERE id = ?", [
|
|
$data['liittymanopeus'] ?? '',
|
|
$data['vlan'] ?? '',
|
|
$data['laite'] ?? '',
|
|
$data['portti'] ?? '',
|
|
$data['ip'] ?? '',
|
|
$data['asennusosoite'] ?? '',
|
|
$data['postinumero'] ?? '',
|
|
$data['kaupunki'] ?? '',
|
|
$connectionId
|
|
]);
|
|
}
|