feat: ticket reply improvements + priority + templates + Telegram

Reply form:
- Mailbox/sender selection dropdown (choose which email to reply from)
- CC field (auto-filled from incoming email CC, editable)
- Reply templates dropdown (quick insert pre-made responses)

Priority system:
- Three levels: normaali, tärkeä, urgent
- Priority dropdown in ticket detail view
- Priority-based sorting (urgent/tärkeä always on top)
- Visual indicators in ticket list (colored rows, emoji badges)
- Priority emails: per-company email list that auto-sets "tärkeä"

Response templates:
- CRUD management in Settings tab
- Dropdown selector in reply form
- Templates insert into textarea

Telegram alerts:
- Bot token + chat ID configuration in Settings
- Test button to verify connection
- Auto-alert on urgent tickets (both manual and from email fetch)
- Alert on priority email matches

Database changes:
- New tables: reply_templates, customer_priority_emails
- New columns: tickets.cc, tickets.priority
- ALTER TABLE migration in initDatabase()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 17:42:05 +02:00
parent 3b7def1186
commit 8485da8cbf
5 changed files with 591 additions and 23 deletions

82
db.php
View File

@@ -246,6 +246,8 @@ function initDatabase(): void {
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,
@@ -328,6 +330,25 @@ function initDatabase(): void {
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 files (
id INT AUTO_INCREMENT PRIMARY KEY,
company_id VARCHAR(50) NOT NULL,
@@ -347,6 +368,15 @@ function initDatabase(): void {
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",
];
foreach ($alters as $sql) {
try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa */ }
}
}
// ==================== YRITYKSET ====================
@@ -769,14 +799,15 @@ function dbSaveTicket(string $companyId, array $ticket): void {
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)
assigned_to, customer_id, customer_name, message_id, mailbox_id, cc, priority, 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)
: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'],
@@ -791,6 +822,8 @@ function dbSaveTicket(string $companyId, array $ticket): void {
'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'),
@@ -1015,3 +1048,48 @@ function dbSetCompanyApiKey(string $companyId, string $apiKey): void {
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 dbLoadPriorityEmails(string $companyId): array {
return _dbFetchColumn("SELECT email FROM customer_priority_emails WHERE company_id = ?", [$companyId]);
}
function dbIsPriorityEmail(string $companyId, string $email): bool {
$email = strtolower(trim($email));
if (!$email) return false;
return (bool)_dbFetchScalar(
"SELECT COUNT(*) FROM customer_priority_emails WHERE company_id = ? AND LOWER(email) = ?",
[$companyId, $email]
);
}