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:
82
db.php
82
db.php
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user