diff --git a/api.php b/api.php index 830a077..d00dfe7 100644 --- a/api.php +++ b/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 , Name2 " 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(); diff --git a/db.php b/db.php index f5adfa5..c6b6f8d 100644 --- a/db.php +++ b/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] + ); +} diff --git a/index.html b/index.html index 4ffadfb..69ff8c2 100644 --- a/index.html +++ b/index.html @@ -328,9 +328,24 @@
-
+
+
+ +
+
+
+
+ + +
+
+ + +
@@ -484,6 +499,61 @@
+ + +
+
+

Vastauspohjat

+ +
+

Nopeat vastauspohjat tiketteihin. Valittavissa vastauslomakkeen valikosta.

+
+ +
+ + +
+

Priority-sähkÜpostiosoitteet

+

Näiltä osoitteilta saapuvat tiketit saavat automaattisesti "Tärkeä"-prioriteetin.

+ +
+ +
+
+ + +
+

Telegram-hälytykset

+

URGENT-prioriteetin tiketit lähettävät hälytyksen Telegram-bottiin.

+
+
+ + +
+
+ + +
+
+ + +
+
+
diff --git a/script.js b/script.js index 09152bb..209364f 100644 --- a/script.js +++ b/script.js @@ -1128,10 +1128,16 @@ function renderTickets() { ); } - // Sorttaus: tila-prioriteetti + päivämäärä + // Sorttaus: prioriteetti → tila → päivämäärä const ticketSortField = document.getElementById('ticket-sort')?.value || 'status'; - const statusPriority = { kasittelyssa: 0, uusi: 1, odottaa: 2, suljettu: 3 }; + const statusPriority = { kasittelyssa: 0, uusi: 1, odottaa: 2, ratkaistu: 3, suljettu: 4 }; + const priorityOrder = { urgent: 0, 'tärkeä': 1, normaali: 2 }; filtered.sort((a, b) => { + // Urgent/tärkeä aina ensin + const prioA = priorityOrder[a.priority || 'normaali'] ?? 2; + const prioB = priorityOrder[b.priority || 'normaali'] ?? 2; + if (prioA !== prioB) return prioA - prioB; + if (ticketSortField === 'status') { const pa = statusPriority[a.status] ?? 9; const pb = statusPriority[b.status] ?? 9; @@ -1158,14 +1164,15 @@ function renderTickets() { ttbody.innerHTML = filtered.map(t => { const lastType = t.last_message_type === 'reply_out' ? '→' : (t.last_message_type === 'note' ? '📝' : '←'); const typeLabel = ticketTypeLabels[t.type] || 'Muu'; - const rowClass = t.status === 'kasittelyssa' ? 'ticket-row-active' : ''; + const rowClass = t.status === 'kasittelyssa' ? 'ticket-row-active' : (t.priority === 'urgent' ? 'ticket-row-urgent' : (t.priority === 'tärkeä' ? 'ticket-row-important' : '')); const checked = bulkSelectedIds.has(t.id) ? 'checked' : ''; const companyBadge = multiCompany && t.company_name ? `${esc(t.company_name)} ` : ''; + const prioBadge = t.priority === 'urgent' ? '🚨 ' : (t.priority === 'tärkeä' ? '⚠️ ' : ''); return ` ${ticketStatusLabels[t.status] || t.status} ${typeLabel} - ${companyBadge}${esc(t.subject)} + ${prioBadge}${companyBadge}${esc(t.subject)} ${esc(t.mailbox_name || t.from_name || t.from_email)} ${t.customer_name ? esc(t.customer_name) : '-'} ${lastType} ${t.message_count} @@ -1253,6 +1260,11 @@ async function showTicketDetail(id, companyId = '') { + @@ -1268,7 +1280,8 @@ async function showTicketDetail(id, companyId = '') { ${ticket.auto_close_at ? '⏰ Auto-close: ' + esc(ticket.auto_close_at.substring(0, 10)) + '' : ''} - `; + + ${ticket.cc ? '
CC: ' + esc(ticket.cc) + '
' : ''}`; // Load users for assignment dropdown try { @@ -1324,6 +1337,15 @@ async function showTicketDetail(id, companyId = '') { } catch (e) { alert(e.message); } }); + // Priority handler + document.getElementById('ticket-priority-select').addEventListener('change', async function() { + try { + await apiCall('ticket_priority' + ticketCompanyParam(), 'POST', { id: currentTicketId, priority: this.value }); + // Päivitä näkymä (visuaalinen muutos) + await showTicketDetail(currentTicketId, currentTicketCompanyId); + } catch (e) { alert(e.message); } + }); + // Delete handler document.getElementById('btn-ticket-delete').addEventListener('click', async () => { if (!confirm('Poistetaanko tiketti "' + ticket.subject + '"?')) return; @@ -1393,17 +1415,58 @@ async function showTicketDetail(id, companyId = '') { document.querySelector('.btn-reply-tab[data-reply-type="reply"]').classList.add('active'); document.getElementById('btn-send-reply').textContent = 'Lähetä vastaus'; - // Allekirjoituksen esikatselu - const sigPreview = document.getElementById('signature-preview'); - const mailboxId = ticket.mailbox_id || ''; - const sig = currentUserSignatures[mailboxId] || ''; - if (sig) { - sigPreview.textContent = '-- \n' + sig; - sigPreview.style.display = 'block'; - } else { - sigPreview.style.display = 'none'; + // CC-kenttä — täytetään tiketin CC:stä + const ccField = document.getElementById('reply-cc'); + if (ccField) ccField.value = ticket.cc || ''; + + // Mailbox-valinta — täytetään yrityksen postilaatikoista + const mbSelect = document.getElementById('reply-mailbox-select'); + if (mbSelect) { + try { + const mailboxes = await apiCall('all_mailboxes'); + mbSelect.innerHTML = mailboxes.map(mb => + `` + ).join(''); + // Vaihda allekirjoitusta kun mailbox vaihtuu + mbSelect.addEventListener('change', function() { + updateSignaturePreview(this.value); + }); + } catch (e) { mbSelect.innerHTML = ''; } } + // Allekirjoituksen esikatselu + function updateSignaturePreview(mbId) { + const sigPreview = document.getElementById('signature-preview'); + const sig = currentUserSignatures[mbId] || ''; + if (sig) { + sigPreview.textContent = '-- \n' + sig; + sigPreview.style.display = 'block'; + } else { + sigPreview.style.display = 'none'; + } + } + updateSignaturePreview(ticket.mailbox_id || ''); + + // Vastauspohjat — lataa dropdown + try { + const templates = await apiCall('reply_templates'); + const tplSelect = document.getElementById('reply-template-select'); + tplSelect.innerHTML = ''; + templates.forEach(t => { + tplSelect.innerHTML += ``; + }); + tplSelect.addEventListener('change', function() { + const opt = this.options[this.selectedIndex]; + const body = opt.dataset.body || ''; + if (body) { + const textarea = document.getElementById('ticket-reply-body'); + textarea.value = textarea.value ? textarea.value + '\n\n' + body : body; + textarea.focus(); + } + this.value = ''; // Reset select + }); + } catch (e) { /* templates not critical */ } + } catch (e) { alert(e.message); } } @@ -1433,13 +1496,19 @@ document.querySelectorAll('.btn-reply-tab').forEach(btn => { const textarea = document.getElementById('ticket-reply-body'); const sendBtn = document.getElementById('btn-send-reply'); const sigPrev = document.getElementById('signature-preview'); + const metaFields = document.getElementById('reply-meta-fields'); + const tplWrap = document.getElementById('reply-template-select-wrap'); if (ticketReplyType === 'note') { textarea.placeholder = 'Kirjoita sisäinen muistiinpano...'; sendBtn.textContent = 'Tallenna muistiinpano'; sigPrev.style.display = 'none'; + if (metaFields) metaFields.style.display = 'none'; + if (tplWrap) tplWrap.style.display = 'none'; } else { textarea.placeholder = 'Kirjoita vastaus...'; sendBtn.textContent = 'Lähetä vastaus'; + if (metaFields) metaFields.style.display = ''; + if (tplWrap) tplWrap.style.display = ''; // Näytä allekirjoitus jos on asetettu if (sigPrev.textContent.trim()) sigPrev.style.display = 'block'; } @@ -1458,7 +1527,14 @@ document.getElementById('btn-send-reply').addEventListener('click', async () => try { const action = ticketReplyType === 'note' ? 'ticket_note' : 'ticket_reply'; - await apiCall(action + ticketCompanyParam(), 'POST', { id: currentTicketId, body }); + const payload = { id: currentTicketId, body }; + if (ticketReplyType !== 'note') { + const mbSel = document.getElementById('reply-mailbox-select'); + const ccFld = document.getElementById('reply-cc'); + if (mbSel) payload.mailbox_id = mbSel.value; + if (ccFld) payload.cc = ccFld.value.trim(); + } + await apiCall(action + ticketCompanyParam(), 'POST', payload); // Reload the detail view await showTicketDetail(currentTicketId, currentTicketCompanyId); } catch (e) { @@ -1709,9 +1785,127 @@ async function loadSettings() { if (apiTitle && currentCompany) apiTitle.textContent = currentCompany.nimi + ' — '; const key = config.api_key || 'AVAIN'; document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${key}&osoite=Kauppakatu+5&postinumero=20100&kaupunki=Turku`; + + // Telegram-asetukset + document.getElementById('settings-telegram-token').value = config.telegram_bot_token || ''; + document.getElementById('settings-telegram-chat').value = config.telegram_chat_id || ''; + } catch (e) { console.error(e); } + + // Vastauspohjat + loadTemplates(); + // Priority emails + loadPriorityEmails(); +} + +// ==================== VASTAUSPOHJAT ==================== + +let replyTemplates = []; + +async function loadTemplates() { + try { + replyTemplates = await apiCall('reply_templates'); + renderTemplates(); } catch (e) { console.error(e); } } +function renderTemplates() { + const list = document.getElementById('templates-list'); + if (!list) return; + if (replyTemplates.length === 0) { + list.innerHTML = '

Ei vastauspohjia vielä.

'; + return; + } + list.innerHTML = replyTemplates.map(t => + `
+
+ ${esc(t.nimi)} +
${esc(t.body.substring(0, 80))}
+
+
+ + +
+
` + ).join(''); +} + +document.getElementById('btn-add-template').addEventListener('click', () => { + document.getElementById('template-edit-id').value = ''; + document.getElementById('template-edit-name').value = ''; + document.getElementById('template-edit-body').value = ''; + document.getElementById('template-form').style.display = 'block'; +}); + +document.getElementById('btn-cancel-template').addEventListener('click', () => { + document.getElementById('template-form').style.display = 'none'; +}); + +document.getElementById('btn-save-template').addEventListener('click', async () => { + const id = document.getElementById('template-edit-id').value || undefined; + const nimi = document.getElementById('template-edit-name').value.trim(); + const body = document.getElementById('template-edit-body').value.trim(); + if (!nimi || !body) { alert('Täytä nimi ja sisältÜ'); return; } + try { + await apiCall('reply_template_save', 'POST', { id, nimi, body }); + document.getElementById('template-form').style.display = 'none'; + loadTemplates(); + } catch (e) { alert(e.message); } +}); + +window.editTemplate = function(id) { + const t = replyTemplates.find(x => x.id === id); + if (!t) return; + document.getElementById('template-edit-id').value = t.id; + document.getElementById('template-edit-name').value = t.nimi; + document.getElementById('template-edit-body').value = t.body; + document.getElementById('template-form').style.display = 'block'; +}; + +window.deleteTemplate = async function(id) { + if (!confirm('Poistetaanko vastauspohja?')) return; + try { + await apiCall('reply_template_delete', 'POST', { id }); + loadTemplates(); + } catch (e) { alert(e.message); } +}; + +// ==================== PRIORITY EMAILS ==================== + +async function loadPriorityEmails() { + try { + const emails = await apiCall('priority_emails'); + document.getElementById('priority-emails-textarea').value = (emails || []).join('\n'); + } catch (e) { console.error(e); } +} + +document.getElementById('btn-save-priority-emails').addEventListener('click', async () => { + const text = document.getElementById('priority-emails-textarea').value; + const emails = text.split('\n').map(e => e.trim()).filter(e => e); + try { + await apiCall('priority_emails_save', 'POST', { emails }); + alert('Priority-osoitteet tallennettu!'); + } catch (e) { alert(e.message); } +}); + +// ==================== TELEGRAM ==================== + +document.getElementById('btn-save-telegram').addEventListener('click', async () => { + try { + await apiCall('config_update', 'POST', { + telegram_bot_token: document.getElementById('settings-telegram-token').value.trim(), + telegram_chat_id: document.getElementById('settings-telegram-chat').value.trim(), + }); + alert('Telegram-asetukset tallennettu!'); + } catch (e) { alert(e.message); } +}); + +document.getElementById('btn-test-telegram').addEventListener('click', async () => { + try { + await apiCall('telegram_test', 'POST'); + alert('Testiviesti lähetetty!'); + } catch (e) { alert(e.message); } +}); + document.getElementById('btn-generate-key').addEventListener('click', async () => { try { const config = await apiCall('generate_api_key', 'POST'); diff --git a/style.css b/style.css index 50ea43b..b59ce7c 100644 --- a/style.css +++ b/style.css @@ -1184,6 +1184,30 @@ span.empty { background: #d5f0d5 !important; } +.ticket-row-urgent { + background: #fef2f2 !important; + border-left: 3px solid #e74c3c !important; +} +.ticket-row-urgent:hover { + background: #fee2e2 !important; +} + +.ticket-row-important { + background: #fffbeb !important; + border-left: 3px solid #f59e0b !important; +} +.ticket-row-important:hover { + background: #fef3c7 !important; +} + +.ticket-prio-urgent { + font-size: 0.9rem; +} + +.ticket-prio-important { + font-size: 0.9rem; +} + /* Ticket thread */ .ticket-thread { display: flex;