Zammad-tiketit: vastaus Zammad API:n kautta + to-osoite + HTML-viestit

1. Zammad-tiketeille näytetään vastaanotto-osoite (esim. support@web1.fi)
   lähettäjäkentässä eikä SMTP-postilaatikkolistaa — vastaus menee
   Zammad API:n kautta.
2. Ensimmäisen artikkelin to-osoite tallennetaan zammad_to_email kenttään
   on-demand artikkelien haussa.
3. Korjattu _dbFetchRow → _dbFetchOne zammad_reply endpointissa.
4. sanitizeHtml() renderöi viestien HTML turvallisesti.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 01:06:43 +02:00
parent d3ab0d3e76
commit 02a5c08164
3 changed files with 37 additions and 24 deletions

11
api.php
View File

@@ -3075,8 +3075,13 @@ switch ($action) {
if ($integ && $integ['enabled']) { if ($integ && $integ['enabled']) {
$z = new ZammadClient($integ['config']['url'], $integ['config']['token']); $z = new ZammadClient($integ['config']['url'], $integ['config']['token']);
$articles = $z->getArticles((int)$ticket['zammad_ticket_id']); $articles = $z->getArticles((int)$ticket['zammad_ticket_id']);
$toEmail = '';
foreach ($articles as $art) { foreach ($articles as $art) {
if (($art['internal'] ?? false)) continue; if (($art['internal'] ?? false)) continue;
// Tallenna ensimmäisen saapuneen viestin to-osoite
if (!$toEmail && ($art['sender'] ?? '') === 'Customer' && !empty($art['to'])) {
$toEmail = $art['to'];
}
$artId = (int)$art['id']; $artId = (int)$art['id'];
$existingMsg = dbGetMessageByZammadArticleId($ticket['id'], $artId); $existingMsg = dbGetMessageByZammadArticleId($ticket['id'], $artId);
if ($existingMsg) continue; if ($existingMsg) continue;
@@ -3094,6 +3099,10 @@ switch ($action) {
$art['message_id'] ?? '', $artId] $art['message_id'] ?? '', $artId]
); );
} }
// Tallenna Zammad to-osoite tikettiin
if ($toEmail) {
_dbExecute("UPDATE tickets SET zammad_to_email = ? WHERE id = ?", [$toEmail, $ticket['id']]);
}
// Lataa tiketti uudelleen viestien kanssa // Lataa tiketti uudelleen viestien kanssa
$tickets = dbLoadTickets($companyId); $tickets = dbLoadTickets($companyId);
foreach ($tickets as $t) { foreach ($tickets as $t) {
@@ -5276,7 +5285,7 @@ switch ($action) {
$body = $input['body'] ?? ''; $body = $input['body'] ?? '';
if (!$ticketId || !$body) { http_response_code(400); echo json_encode(['error' => 'Tiketti ja viesti vaaditaan']); break; } if (!$ticketId || !$body) { http_response_code(400); echo json_encode(['error' => 'Tiketti ja viesti vaaditaan']); break; }
$ticket = _dbFetchRow("SELECT * FROM tickets WHERE id = ? AND company_id = ?", [$ticketId, $companyId]); $ticket = _dbFetchOne("SELECT * FROM tickets WHERE id = ? AND company_id = ?", [$ticketId, $companyId]);
if (!$ticket || !$ticket['zammad_ticket_id']) { if (!$ticket || !$ticket['zammad_ticket_id']) {
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'Tiketti ei ole Zammad-tiketti']); echo json_encode(['error' => 'Tiketti ei ole Zammad-tiketti']);

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Noxus HUB</title> <title>Noxus HUB</title>
<link rel="stylesheet" href="style.css?v=20260313b"> <link rel="stylesheet" href="style.css?v=20260313c">
</head> </head>
<body> <body>
<!-- Login --> <!-- Login -->
@@ -2229,6 +2229,6 @@
</div> </div>
</div> </div>
<script src="script.js?v=20260313b"></script> <script src="script.js?v=20260313c"></script>
</body> </body>
</html> </html>

View File

@@ -1846,12 +1846,16 @@ async function showTicketDetail(id, companyId = '') {
const ccField = document.getElementById('reply-cc'); const ccField = document.getElementById('reply-cc');
if (ccField) ccField.value = ticket.cc || ''; if (ccField) ccField.value = ticket.cc || '';
// Mailbox-valinta — täytetään yrityksen postilaatikoista // Mailbox-valinta — Zammad-tiketit vastaa Zammadin kautta, muut SMTP:llä
const mbSelect = document.getElementById('reply-mailbox-select'); const mbSelect = document.getElementById('reply-mailbox-select');
if (mbSelect) { if (mbSelect) {
if (ticket.source === 'zammad' && ticket.zammad_ticket_id) {
// Zammad-tiketti: vastaus menee Zammadin kautta
const zTo = ticket.zammad_to_email || ticket.zammad_group || 'Zammad';
mbSelect.innerHTML = `<option value="zammad" selected>${esc(zTo)} (Zammad)</option>`;
} else {
try { try {
const mailboxes = await apiCall('all_mailboxes'); const mailboxes = await apiCall('all_mailboxes');
// Suodata pois piilotetut postilaatikot (paitsi jos tiketin oma mailbox on piilotettu — se näytetään silti)
const visibleMailboxes = mailboxes.filter(mb => const visibleMailboxes = mailboxes.filter(mb =>
String(mb.id) === String(ticket.mailbox_id || '') || String(mb.id) === String(ticket.mailbox_id || '') ||
(!currentHiddenMailboxes.includes(String(mb.id)) && !currentHiddenMailboxes.includes(mb.id)) (!currentHiddenMailboxes.includes(String(mb.id)) && !currentHiddenMailboxes.includes(mb.id))
@@ -1864,11 +1868,11 @@ async function showTicketDetail(id, companyId = '') {
`<option value="${esc(mb.id)}" ${String(mb.id) === ticketMbId ? 'selected' : ''}>${esc(mb.nimi || mb.smtp_from_email)} &lt;${esc(mb.smtp_from_email)}&gt;</option>` `<option value="${esc(mb.id)}" ${String(mb.id) === ticketMbId ? 'selected' : ''}>${esc(mb.nimi || mb.smtp_from_email)} &lt;${esc(mb.smtp_from_email)}&gt;</option>`
).join(''); ).join('');
mbSelect.innerHTML = optionsHtml; mbSelect.innerHTML = optionsHtml;
// Vaihda allekirjoitusta kun mailbox vaihtuu } catch (e) { mbSelect.innerHTML = '<option>Ei postilaatikoita</option>'; }
}
mbSelect.addEventListener('change', function() { mbSelect.addEventListener('change', function() {
updateSignaturePreview(this.value); updateSignaturePreview(this.value);
}); });
} catch (e) { mbSelect.innerHTML = '<option>Ei postilaatikoita</option>'; }
} }
// Allekirjoituksen esikatselu // Allekirjoituksen esikatselu