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:
212
api.php
212
api.php
@@ -262,6 +262,10 @@ class ImapClient {
|
||||
$date = $dateStr ? @date('Y-m-d H:i:s', strtotime($dateStr)) : date('Y-m-d H:i:s');
|
||||
if (!$date) $date = date('Y-m-d H:i:s');
|
||||
|
||||
// Parse CC
|
||||
$ccRaw = $this->decodeMimeHeader($headers['cc'] ?? '');
|
||||
$ccEmails = $this->parseCcAddresses($ccRaw);
|
||||
|
||||
// Fetch body (text part)
|
||||
$body = $this->fetchBody($num);
|
||||
|
||||
@@ -274,6 +278,7 @@ class ImapClient {
|
||||
'references' => $references,
|
||||
'date' => $date,
|
||||
'body' => $body,
|
||||
'cc' => $ccEmails,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -376,6 +381,22 @@ class ImapClient {
|
||||
return ['name' => '', 'email' => $from];
|
||||
}
|
||||
|
||||
private function parseCcAddresses(string $cc): string {
|
||||
$cc = trim($cc);
|
||||
if (!$cc) return '';
|
||||
// Parse "Name <email>, Name2 <email2>" or "email1, email2"
|
||||
$emails = [];
|
||||
// Split on comma, but be careful with quoted strings
|
||||
$parts = preg_split('/,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)/', $cc);
|
||||
foreach ($parts as $part) {
|
||||
$part = trim($part);
|
||||
if (!$part) continue;
|
||||
$parsed = $this->parseFrom($part);
|
||||
if ($parsed['email']) $emails[] = $parsed['email'];
|
||||
}
|
||||
return implode(', ', $emails);
|
||||
}
|
||||
|
||||
private function decodeMimeHeader(string $str): string {
|
||||
if (strpos($str, '=?') === false) return trim($str);
|
||||
$decoded = '';
|
||||
@@ -496,7 +517,39 @@ class ImapClient {
|
||||
|
||||
// ==================== TICKETS HELPER ====================
|
||||
|
||||
function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null): bool {
|
||||
function sendTelegramAlert(string $companyId, array $ticket): void {
|
||||
$config = dbLoadConfig();
|
||||
$botToken = $config['telegram_bot_token'] ?? '';
|
||||
$chatId = $config['telegram_chat_id'] ?? '';
|
||||
if (!$botToken || !$chatId) return;
|
||||
|
||||
$text = "🚨 *URGENT TIKETTI*\n\n";
|
||||
$text .= "📋 *" . ($ticket['subject'] ?? '(Ei aihetta)') . "*\n";
|
||||
$text .= "👤 " . ($ticket['from_name'] ?? $ticket['from_email'] ?? 'Tuntematon') . "\n";
|
||||
$text .= "📧 " . ($ticket['from_email'] ?? '') . "\n";
|
||||
$text .= "🏢 " . $companyId . "\n";
|
||||
$text .= "🕐 " . date('d.m.Y H:i');
|
||||
|
||||
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
|
||||
$data = [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $text,
|
||||
'parse_mode' => 'Markdown',
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($data),
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null, string $cc = ''): bool {
|
||||
$fromEmail = $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? MAIL_FROM;
|
||||
$fromName = $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? 'Asiakaspalvelu';
|
||||
|
||||
@@ -504,6 +557,9 @@ function sendTicketMail(string $to, string $subject, string $body, string $inRep
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
$headers .= "From: {$fromName} <{$fromEmail}>\r\n";
|
||||
$headers .= "Reply-To: {$fromEmail}\r\n";
|
||||
if ($cc) {
|
||||
$headers .= "Cc: {$cc}\r\n";
|
||||
}
|
||||
if ($inReplyTo) {
|
||||
$headers .= "In-Reply-To: {$inReplyTo}\r\n";
|
||||
$headers .= "References: " . ($references ? $references . ' ' : '') . $inReplyTo . "\r\n";
|
||||
@@ -599,9 +655,12 @@ switch ($action) {
|
||||
case 'config':
|
||||
requireAdmin();
|
||||
$companyId = requireCompany();
|
||||
$globalConf = dbLoadConfig();
|
||||
echo json_encode([
|
||||
'api_key' => dbGetCompanyApiKey($companyId),
|
||||
'cors_origins' => dbGetCompanyCorsOrigins($companyId),
|
||||
'telegram_bot_token' => $globalConf['telegram_bot_token'] ?? '',
|
||||
'telegram_chat_id' => $globalConf['telegram_chat_id'] ?? '',
|
||||
]);
|
||||
break;
|
||||
|
||||
@@ -617,13 +676,46 @@ switch ($action) {
|
||||
$origins = array_filter(array_map('trim', explode("\n", $input['cors_origins'])));
|
||||
dbSetCompanyCorsOrigins($companyId, array_values($origins));
|
||||
}
|
||||
dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Päivitti API-asetukset');
|
||||
// Telegram-asetukset (globaalit, tallennetaan config-tauluun)
|
||||
if (isset($input['telegram_bot_token'])) {
|
||||
dbSaveConfig(['telegram_bot_token' => trim($input['telegram_bot_token'])]);
|
||||
}
|
||||
if (isset($input['telegram_chat_id'])) {
|
||||
dbSaveConfig(['telegram_chat_id' => trim($input['telegram_chat_id'])]);
|
||||
}
|
||||
dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Päivitti asetuksia');
|
||||
echo json_encode([
|
||||
'api_key' => dbGetCompanyApiKey($companyId),
|
||||
'cors_origins' => dbGetCompanyCorsOrigins($companyId),
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'telegram_test':
|
||||
requireAdmin();
|
||||
if ($method !== 'POST') break;
|
||||
$config = dbLoadConfig();
|
||||
$botToken = $config['telegram_bot_token'] ?? '';
|
||||
$chatId = $config['telegram_chat_id'] ?? '';
|
||||
if (!$botToken || !$chatId) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Telegram Bot Token ja Chat ID vaaditaan']);
|
||||
break;
|
||||
}
|
||||
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
|
||||
$data = ['chat_id' => $chatId, 'text' => '✅ Noxus Intra Telegram-hälytys toimii!', 'parse_mode' => 'Markdown'];
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
|
||||
$resp = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($httpCode === 200) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Telegram virhe: ' . $resp]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'generate_api_key':
|
||||
requireAdmin();
|
||||
$companyId = requireCompany();
|
||||
@@ -1639,6 +1731,12 @@ switch ($action) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Tarkista onko lähettäjä priority-listalla
|
||||
$ticketPriority = 'normaali';
|
||||
if (dbIsPriorityEmail($companyId, $email['from_email'])) {
|
||||
$ticketPriority = 'tärkeä';
|
||||
}
|
||||
|
||||
$ticket = [
|
||||
'id' => generateId(),
|
||||
'subject' => $email['subject'] ?: '(Ei aihetta)',
|
||||
@@ -1650,6 +1748,8 @@ switch ($action) {
|
||||
'customer_id' => '',
|
||||
'customer_name' => '',
|
||||
'tags' => [],
|
||||
'cc' => $email['cc'] ?? '',
|
||||
'priority' => $ticketPriority,
|
||||
'auto_close_at' => '',
|
||||
'mailbox_id' => $mailbox['id'],
|
||||
'created' => $email['date'],
|
||||
@@ -1692,6 +1792,10 @@ switch ($action) {
|
||||
}
|
||||
|
||||
dbSaveTicket($companyId, $ticket);
|
||||
// Telegram-hälytys tärkeille/urgentille
|
||||
if ($ticket['priority'] === 'urgent' || $ticket['priority'] === 'tärkeä') {
|
||||
sendTelegramAlert($companyId, $ticket);
|
||||
}
|
||||
$newCount++;
|
||||
}
|
||||
|
||||
@@ -1714,6 +1818,8 @@ switch ($action) {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
$body = trim($input['body'] ?? '');
|
||||
$replyMailboxId = $input['mailbox_id'] ?? '';
|
||||
$replyCc = trim($input['cc'] ?? '');
|
||||
if (empty($body)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Viesti ei voi olla tyhjä']);
|
||||
@@ -1734,10 +1840,12 @@ switch ($action) {
|
||||
}
|
||||
|
||||
// Send email — hae postilaatikon asetukset
|
||||
// Käytä frontendistä valittua mailboxia tai tiketin oletusta
|
||||
$companyConf = dbGetCompanyConfig($companyId);
|
||||
$useMailboxId = $replyMailboxId ?: ($t['mailbox_id'] ?? '');
|
||||
$replyMailbox = null;
|
||||
foreach ($companyConf['mailboxes'] ?? [] as $mb) {
|
||||
if ($mb['id'] === ($t['mailbox_id'] ?? '')) { $replyMailbox = $mb; break; }
|
||||
if ($mb['id'] === $useMailboxId) { $replyMailbox = $mb; break; }
|
||||
}
|
||||
// Fallback: käytä ensimmäistä postilaatikkoa
|
||||
if (!$replyMailbox && !empty($companyConf['mailboxes'])) {
|
||||
@@ -1745,7 +1853,7 @@ switch ($action) {
|
||||
}
|
||||
|
||||
// Hae käyttäjän allekirjoitus tälle postilaatikolle
|
||||
$mailboxId = $t['mailbox_id'] ?? '';
|
||||
$mailboxId = $replyMailbox['id'] ?? '';
|
||||
$signature = '';
|
||||
$sigUser = dbGetUser($_SESSION['user_id']);
|
||||
if ($sigUser) {
|
||||
@@ -1753,8 +1861,11 @@ switch ($action) {
|
||||
}
|
||||
$emailBody = $signature ? $body . "\n\n-- \n" . $signature : $body;
|
||||
|
||||
// CC: käytä frontendistä annettua CC:tä, tai tiketin alkuperäistä CC:tä
|
||||
$ccToSend = $replyCc !== '' ? $replyCc : ($t['cc'] ?? '');
|
||||
|
||||
$subject = 'Re: ' . $t['subject'];
|
||||
$sent = sendTicketMail($t['from_email'], $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox);
|
||||
$sent = sendTicketMail($t['from_email'], $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox, $ccToSend);
|
||||
|
||||
if (!$sent) {
|
||||
http_response_code(500);
|
||||
@@ -1762,6 +1873,11 @@ switch ($action) {
|
||||
break 2;
|
||||
}
|
||||
|
||||
// Päivitä tiketin CC jos muuttunut
|
||||
if ($replyCc !== '' && $replyCc !== ($t['cc'] ?? '')) {
|
||||
$t['cc'] = $replyCc;
|
||||
}
|
||||
|
||||
// Add reply to ticket (tallennetaan allekirjoituksen kanssa)
|
||||
$reply = [
|
||||
'id' => generateId(),
|
||||
@@ -2082,6 +2198,92 @@ switch ($action) {
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'ticket_priority':
|
||||
requireAuth();
|
||||
$companyId = requireCompanyOrParam();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
$priority = $input['priority'] ?? 'normaali';
|
||||
if (!in_array($priority, ['normaali', 'tärkeä', 'urgent'])) $priority = 'normaali';
|
||||
$tickets = dbLoadTickets($companyId);
|
||||
foreach ($tickets as $t) {
|
||||
if ($t['id'] === $id) {
|
||||
$t['priority'] = $priority;
|
||||
$t['updated'] = date('Y-m-d H:i:s');
|
||||
dbSaveTicket($companyId, $t);
|
||||
// Telegram-hälytys urgentille
|
||||
if ($priority === 'urgent') {
|
||||
sendTelegramAlert($companyId, $t);
|
||||
}
|
||||
echo json_encode($t);
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
||||
break;
|
||||
|
||||
// ---------- VASTAUSPOHJAT ----------
|
||||
case 'reply_templates':
|
||||
requireAuth();
|
||||
$companyId = requireCompany();
|
||||
echo json_encode(dbLoadTemplates($companyId));
|
||||
break;
|
||||
|
||||
case 'reply_template_save':
|
||||
requireAuth();
|
||||
$companyId = requireCompany();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($input['nimi']) || empty($input['body'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Nimi ja sisältö vaaditaan']);
|
||||
break;
|
||||
}
|
||||
$tpl = [
|
||||
'id' => $input['id'] ?? generateId(),
|
||||
'nimi' => trim($input['nimi']),
|
||||
'body' => trim($input['body']),
|
||||
'sort_order' => intval($input['sort_order'] ?? 0),
|
||||
];
|
||||
dbSaveTemplate($companyId, $tpl);
|
||||
echo json_encode(['success' => true, 'template' => $tpl]);
|
||||
break;
|
||||
|
||||
case 'reply_template_delete':
|
||||
requireAuth();
|
||||
$companyId = requireCompany();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
dbDeleteTemplate($input['id'] ?? '');
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
// ---------- PRIORITY EMAILS ----------
|
||||
case 'priority_emails':
|
||||
requireAuth();
|
||||
$companyId = requireCompany();
|
||||
echo json_encode(dbLoadPriorityEmails($companyId));
|
||||
break;
|
||||
|
||||
case 'priority_emails_save':
|
||||
requireAuth();
|
||||
$companyId = requireCompany();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$emails = $input['emails'] ?? [];
|
||||
// Poista vanhat ja lisää uudet
|
||||
_dbExecute("DELETE FROM customer_priority_emails WHERE company_id = ?", [$companyId]);
|
||||
foreach ($emails as $email) {
|
||||
$email = strtolower(trim($email));
|
||||
if ($email) {
|
||||
_dbExecute("INSERT IGNORE INTO customer_priority_emails (company_id, email) VALUES (?, ?)", [$companyId, $email]);
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
// ---------- COMPANY MANAGEMENT ----------
|
||||
case 'companies':
|
||||
requireAuth();
|
||||
|
||||
Reference in New Issue
Block a user