diff --git a/api.php b/api.php index 108a118..9269e5e 100644 --- a/api.php +++ b/api.php @@ -2195,9 +2195,15 @@ switch ($action) { $ticketPriority = 'tärkeä'; } + // Generoi tikettinumero (VVNKKNN) + $ticketNumber = dbNextTicketNumber($companyId); + $originalSubject = $email['subject'] ?: '(Ei aihetta)'; + $numberedSubject = "Tiketti #{$ticketNumber}: {$originalSubject}"; + $ticket = [ 'id' => generateId(), - 'subject' => $email['subject'] ?: '(Ei aihetta)', + 'ticket_number' => $ticketNumber, + 'subject' => $numberedSubject, 'from_email' => $email['from_email'], 'from_name' => $email['from_name'], 'status' => 'uusi', @@ -2250,6 +2256,36 @@ switch ($action) { } dbSaveTicket($companyId, $ticket); + + // Autoreply — lähetä automaattinen vastaus asiakkaalle + if (!empty($mailbox['auto_reply_enabled']) && !empty($mailbox['auto_reply_body'])) { + $arSubject = 'Re: ' . $ticket['subject']; + $arBody = $mailbox['auto_reply_body']; + $arSent = sendTicketMail( + $email['from_email'], + $arSubject, + $arBody, + $email['message_id'], // In-Reply-To + $email['message_id'], // References + $mailbox, + '' // ei CC + ); + if ($arSent) { + // Tallenna autoreply tiketin viestiksi + $arMsg = [ + 'id' => generateId(), + 'type' => 'auto_reply', + 'from' => $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? '', + 'from_name' => $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? '', + 'body' => $arBody, + 'timestamp' => date('Y-m-d H:i:s'), + 'message_id' => '', + ]; + $ticket['messages'][] = $arMsg; + dbSaveTicket($companyId, $ticket); + } + } + // Telegram-hälytys tärkeille/urgentille if ($ticket['priority'] === 'urgent' || $ticket['priority'] === 'tärkeä') { sendTelegramAlert($companyId, $ticket); @@ -3145,6 +3181,8 @@ switch ($action) { 'smtp_user' => trim($input['smtp_user'] ?? ''), 'smtp_encryption' => trim($input['smtp_encryption'] ?? 'tls'), 'aktiivinen' => $input['aktiivinen'] ?? true, + 'auto_reply_enabled' => !empty($input['auto_reply_enabled']), + 'auto_reply_body' => trim($input['auto_reply_body'] ?? ''), ]; // Hae vanha mailbox salasanojen vertailua varten $existingMb = dbGetMailbox($mb['id']); diff --git a/db.php b/db.php index 2aa5ae5..1b79d83 100644 --- a/db.php +++ b/db.php @@ -236,6 +236,7 @@ function initDatabase(): void { "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), @@ -317,6 +318,8 @@ function initDatabase(): void { 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", @@ -441,6 +444,9 @@ function initDatabase(): void { "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", ]; foreach ($alters as $sql) { try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } @@ -986,6 +992,24 @@ function dbDeleteLead(string $leadId): void { // ==================== 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]); @@ -1017,9 +1041,9 @@ function dbSaveTicket(string $companyId, array $ticket): void { $db->begin_transaction(); try { _dbExecute(" - INSERT INTO tickets (id, company_id, subject, from_email, from_name, status, type, + 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, :company_id, :subject, :from_email, :from_name, :status, :type, + 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), @@ -1030,6 +1054,7 @@ function dbSaveTicket(string $companyId, array $ticket): void { 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'] ?? '', @@ -1157,6 +1182,8 @@ 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']); @@ -1166,15 +1193,16 @@ function dbLoadMailboxes(string $companyId): array { 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) - 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) + 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) + 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, @@ -1190,8 +1218,10 @@ function dbSaveMailbox(string $companyId, array $mailbox): void { '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, + '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'] ?? '', ]); } diff --git a/index.html b/index.html index 45a733f..0e2b6eb 100644 --- a/index.html +++ b/index.html @@ -856,6 +856,18 @@ + +
+ + +
diff --git a/script.js b/script.js index f4052a0..00ee642 100644 --- a/script.js +++ b/script.js @@ -1295,7 +1295,7 @@ function renderTickets() { ${ticketStatusLabels[t.status] || t.status} ${typeLabel} - ${prioBadge}${companyBadge}${esc(t.subject)} + ${prioBadge}${companyBadge}${t.ticket_number ? `#${t.ticket_number}` : ''}${esc(t.subject)} ${esc(t.mailbox_name || t.from_name || t.from_email)} ${t.customer_name ? esc(t.customer_name) : '-'} ${lastType} ${t.message_count} @@ -1361,7 +1361,7 @@ async function showTicketDetail(id, companyId = '') { document.getElementById('ticket-detail-header').innerHTML = `
-

${esc(ticket.subject)}

+

${ticket.ticket_number ? `#${ticket.ticket_number} ` : ''}${esc(ticket.subject)}

${esc(ticket.from_name)} <${esc(ticket.from_email)}> · Luotu ${esc(ticket.created)}
@@ -1552,9 +1552,10 @@ async function showTicketDetail(id, companyId = '') { const thread = document.getElementById('ticket-thread'); thread.innerHTML = (ticket.messages || []).map(m => { const isOut = m.type === 'reply_out'; + const isAutoReply = m.type === 'auto_reply'; const isNote = m.type === 'note'; - const typeClass = isOut ? 'ticket-msg-out' : (isNote ? 'ticket-msg-note' : 'ticket-msg-in'); - const typeIcon = isOut ? '→ Vastaus' : (isNote ? '📝 Muistiinpano' : '← Saapunut'); + const typeClass = (isOut || isAutoReply) ? 'ticket-msg-out' : (isNote ? 'ticket-msg-note' : 'ticket-msg-in'); + const typeIcon = isAutoReply ? '⚡ Automaattinen vastaus' : (isOut ? '→ Vastaus' : (isNote ? '📝 Muistiinpano' : '← Saapunut')); return `
${typeIcon} @@ -2541,7 +2542,13 @@ function showMailboxForm(mb = null) { } else { sameCheck.checked = true; } + // Autoreply + const arCheck = document.getElementById('mailbox-form-auto-reply'); + arCheck.checked = mb ? !!mb.auto_reply_enabled : false; + document.getElementById('mailbox-form-auto-reply-body').value = mb ? (mb.auto_reply_body || '') : ''; + toggleAutoReplyFields(); toggleSmtpFields(); + document.getElementById('smtp-test-result').style.display = 'none'; document.getElementById('mailbox-form-container').style.display = ''; } @@ -2550,6 +2557,12 @@ function toggleSmtpFields() { document.getElementById('smtp-custom-fields').style.display = same ? 'none' : ''; } +function toggleAutoReplyFields() { + const enabled = document.getElementById('mailbox-form-auto-reply').checked; + document.getElementById('auto-reply-fields').style.display = enabled ? '' : 'none'; +} +document.getElementById('mailbox-form-auto-reply').addEventListener('change', toggleAutoReplyFields); + function editMailbox(id) { const mb = mailboxesData.find(m => m.id === id); if (mb) showMailboxForm(mb); @@ -2588,6 +2601,8 @@ document.getElementById('btn-save-mailbox').addEventListener('click', async () = smtp_password: useSame ? imapPass : document.getElementById('mailbox-form-smtp-pass').value, smtp_encryption: document.getElementById('mailbox-form-smtp-encryption').value, aktiivinen: true, + auto_reply_enabled: document.getElementById('mailbox-form-auto-reply').checked, + auto_reply_body: document.getElementById('mailbox-form-auto-reply-body').value, }; try { const saved = await apiCall('mailbox_save', 'POST', data);