Liitetiedostot tikettivastausten sähköposteihin + muistiinpanojen piilotus

- Lisää liitetiedostojen tuki vastauslomakkeeseen (📎 Liitä tiedosto -nappi)
- Tuki max 25 MB tiedostoille, useita liitteitä kerralla
- Zammad API: liitteet base64-muodossa attachments-kentässä
- SMTP: multipart/mixed MIME (boundary, Content-Disposition: attachment)
- Sisäiset muistiinpanot (note) suodatetaan pois quoted threadista
  (eivät näy asiakkaille lähtevissä sähköposteissa)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 03:00:30 +02:00
parent 6f1d9ed5d4
commit 376912b9ff
3 changed files with 120 additions and 14 deletions

View File

@@ -2002,6 +2002,50 @@ document.querySelectorAll('.btn-reply-tab').forEach(btn => {
});
});
// ---- Liitetiedostot ----
let replyAttachments = []; // [{name, size, type, data (base64)}]
document.getElementById('reply-file-input').addEventListener('change', function() {
const files = Array.from(this.files);
files.forEach(file => {
if (file.size > 25 * 1024 * 1024) {
alert(`Tiedosto "${file.name}" on liian suuri (max 25 MB)`);
return;
}
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1]; // poista data:...;base64, prefix
replyAttachments.push({ name: file.name, size: file.size, type: file.type || 'application/octet-stream', data: base64 });
renderReplyAttachments();
};
reader.readAsDataURL(file);
});
this.value = ''; // nollaa input jotta saman tiedoston voi lisätä uudelleen
});
function renderReplyAttachments() {
const list = document.getElementById('reply-attachments-list');
const count = document.getElementById('reply-attachments-count');
if (replyAttachments.length === 0) {
list.innerHTML = '';
count.textContent = '';
return;
}
count.textContent = `${replyAttachments.length} liite${replyAttachments.length > 1 ? 'ttä' : ''}`;
list.innerHTML = replyAttachments.map((a, i) => {
const sizeStr = a.size < 1024 ? a.size + ' B' : (a.size < 1048576 ? (a.size / 1024).toFixed(1) + ' KB' : (a.size / 1048576).toFixed(1) + ' MB');
return `<span style="display:inline-flex;align-items:center;gap:0.3rem;background:#f0f2f5;padding:3px 8px;border-radius:12px;font-size:0.8rem;">
📄 ${esc(a.name)} <span style="color:#aaa;">(${sizeStr})</span>
<button onclick="removeReplyAttachment(${i})" style="background:none;border:none;cursor:pointer;color:#c00;font-size:0.9rem;padding:0 2px;" title="Poista">×</button>
</span>`;
}).join('');
}
function removeReplyAttachment(index) {
replyAttachments.splice(index, 1);
renderReplyAttachments();
}
// Send reply or note
document.getElementById('btn-send-reply').addEventListener('click', async () => {
const body = document.getElementById('ticket-reply-body').value.trim();
@@ -2039,6 +2083,7 @@ document.getElementById('btn-send-reply').addEventListener('click', async () =>
if (zToFld && zToFld.value.trim()) zPayload.to = zToFld.value.trim();
if (zCcFld && zCcFld.value.trim()) zPayload.cc = zCcFld.value.trim();
if (zBccFld && zBccFld.value.trim()) zPayload.bcc = zBccFld.value.trim();
if (replyAttachments.length > 0) zPayload.attachments = replyAttachments;
await apiCall('zammad_reply' + ticketCompanyParam(), 'POST', zPayload);
} else {
const action = ticketReplyType === 'note' ? 'ticket_note' : 'ticket_reply';
@@ -2055,8 +2100,12 @@ document.getElementById('btn-send-reply').addEventListener('click', async () =>
if (bccFld) payload.bcc = bccFld.value.trim();
if (useSig && !useSig.checked) payload.no_signature = true;
}
if (replyAttachments.length > 0) payload.attachments = replyAttachments;
await apiCall(action + ticketCompanyParam(), 'POST', payload);
}
// Tyhjennä liitteet
replyAttachments = [];
renderReplyAttachments();
// Reload the detail view
await showTicketDetail(currentTicketId, currentTicketCompanyId);
} catch (e) {