From 959c9b440105170be4ca72b6c4f70d76984796fe Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 6 Mar 2026 16:11:05 +0100 Subject: [PATCH] Fix: restore case email compose button in sag email tab --- RELEASE_NOTES_v2.2.50.md | 18 ++ VERSION | 2 +- app/modules/sag/templates/detail.html | 235 +++++++++++++++++++++++++- 3 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 RELEASE_NOTES_v2.2.50.md diff --git a/RELEASE_NOTES_v2.2.50.md b/RELEASE_NOTES_v2.2.50.md new file mode 100644 index 0000000..71e4f3f --- /dev/null +++ b/RELEASE_NOTES_v2.2.50.md @@ -0,0 +1,18 @@ +# Release Notes v2.2.50 + +Dato: 6. marts 2026 + +## Fixes +- Sag: “Ny email”-compose er gendannet i E-mail-fanen på sager. +- Tilføjet synlig compose-sektion med felter for Til/Cc/Bcc/Emne/Besked samt vedhæftning af sagsfiler. +- Knap `Ny email` er nu koblet til afsendelse via `/api/v1/sag/{sag_id}/emails/send`. +- Compose prefill’er modtager (primær kontakt hvis muligt) og emne (`Sag #:`). +- Vedhæftningslisten opdateres fra sagsfiler, også når filpanelet ikke er synligt. + +## Ændrede filer +- `app/modules/sag/templates/detail.html` +- `VERSION` +- `RELEASE_NOTES_v2.2.50.md` + +## Drift +- Deploy: `./updateto.sh v2.2.50` diff --git a/VERSION b/VERSION index b5c8a81..99572f6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.48 +2.2.50 diff --git a/app/modules/sag/templates/detail.html b/app/modules/sag/templates/detail.html index fe98066..695175f 100644 --- a/app/modules/sag/templates/detail.html +++ b/app/modules/sag/templates/detail.html @@ -3553,6 +3553,44 @@
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
@@ -6346,32 +6384,70 @@ // ========================================== // FILES & EMAILS LOGIC // ========================================== + + let sagFilesCache = []; // ---------------- FILES ---------------- + + function updateCaseEmailAttachmentOptions(files) { + const select = document.getElementById('caseEmailAttachmentIds'); + if (!select) return; + + const safeFiles = Array.isArray(files) ? files : []; + if (!safeFiles.length) { + select.innerHTML = ''; + return; + } + + select.innerHTML = safeFiles.map((file) => { + const fileId = Number(file.id); + const filename = escapeHtml(file.filename || `Fil ${fileId}`); + const date = file.created_at ? new Date(file.created_at).toLocaleDateString('da-DK') : '-'; + return ``; + }).join(''); + } async function loadSagFiles() { const container = document.getElementById('files-list'); - if(!container) return; - container.innerHTML = '
Henter filer...
'; + if (container) { + container.innerHTML = '
Henter filer...
'; + } try { const res = await fetch(`/api/v1/sag/${caseIds}/files`); if(res.ok) { const files = await res.json(); + sagFilesCache = Array.isArray(files) ? files : []; + updateCaseEmailAttachmentOptions(sagFilesCache); renderFiles(files); } else { - container.innerHTML = '
Fejl ved hentning af filer
'; + sagFilesCache = []; + updateCaseEmailAttachmentOptions(sagFilesCache); + if (container) { + container.innerHTML = '
Fejl ved hentning af filer
'; + } setModuleContentState('files', true); } } catch(e) { console.error(e); - container.innerHTML = '
Fejl ved hentning af filer
'; + sagFilesCache = []; + updateCaseEmailAttachmentOptions(sagFilesCache); + if (container) { + container.innerHTML = '
Fejl ved hentning af filer
'; + } setModuleContentState('files', true); } } function renderFiles(files) { const container = document.getElementById('files-list'); + sagFilesCache = Array.isArray(files) ? files : []; + updateCaseEmailAttachmentOptions(sagFilesCache); + + if (!container) { + return; + } + if(!files || files.length === 0) { container.innerHTML = '
Ingen filer fundet...
'; setModuleContentState('files', false); @@ -6542,6 +6618,150 @@ let linkedEmailsCache = []; let selectedLinkedEmailId = null; + function parseEmailField(value) { + return String(value || '') + .split(/[\n,;]+/) + .map((email) => email.trim()) + .filter(Boolean); + } + + function escapeHtmlForInput(value) { + return String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function getDefaultCaseRecipient() { + const primaryContact = document.querySelector('.contact-row[data-is-primary="true"][data-email]'); + if (primaryContact?.dataset?.email) { + return primaryContact.dataset.email.trim(); + } + + const anyContact = document.querySelector('.contact-row[data-email]'); + if (anyContact?.dataset?.email) { + return anyContact.dataset.email.trim(); + } + + const customerSmall = document.querySelector('.customer-row small'); + if (customerSmall) { + const text = customerSmall.textContent || ''; + const match = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i); + if (match) { + return match[0].trim(); + } + } + + return ''; + } + + function prefillCaseEmailCompose() { + const toInput = document.getElementById('caseEmailTo'); + const subjectInput = document.getElementById('caseEmailSubject'); + + if (toInput && !toInput.value.trim()) { + const recipient = getDefaultCaseRecipient(); + if (recipient) { + toInput.value = recipient; + } + } + + if (subjectInput && !subjectInput.value.trim()) { + subjectInput.value = escapeHtmlForInput(`Sag #${caseIds}: `); + } + } + + async function sendCaseEmail() { + const toInput = document.getElementById('caseEmailTo'); + const ccInput = document.getElementById('caseEmailCc'); + const bccInput = document.getElementById('caseEmailBcc'); + const subjectInput = document.getElementById('caseEmailSubject'); + const bodyInput = document.getElementById('caseEmailBody'); + const attachmentSelect = document.getElementById('caseEmailAttachmentIds'); + const sendBtn = document.getElementById('caseEmailSendBtn'); + const statusEl = document.getElementById('caseEmailSendStatus'); + + if (!toInput || !subjectInput || !bodyInput || !sendBtn || !statusEl) { + return; + } + + const to = parseEmailField(toInput.value); + const cc = parseEmailField(ccInput?.value || ''); + const bcc = parseEmailField(bccInput?.value || ''); + const subject = (subjectInput.value || '').trim(); + const bodyText = (bodyInput.value || '').trim(); + const attachmentFileIds = Array.from(attachmentSelect?.selectedOptions || []) + .map((opt) => Number(opt.value)) + .filter((id) => Number.isInteger(id) && id > 0); + + if (!to.length) { + alert('Udfyld mindst én modtager i Til-feltet.'); + return; + } + + if (!subject) { + alert('Udfyld emne før afsendelse.'); + return; + } + + if (!bodyText) { + alert('Udfyld besked før afsendelse.'); + return; + } + + sendBtn.disabled = true; + statusEl.className = 'text-muted'; + statusEl.textContent = 'Sender e-mail...'; + + try { + const res = await fetch(`/api/v1/sag/${caseIds}/emails/send`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + to, + cc, + bcc, + subject, + body_text: bodyText, + attachment_file_ids: attachmentFileIds + }) + }); + + if (!res.ok) { + let message = 'Kunne ikke sende e-mail.'; + try { + const err = await res.json(); + if (err?.detail) { + message = err.detail; + } + } catch (_) { + } + throw new Error(message); + } + + if (subjectInput) subjectInput.value = ''; + if (bodyInput) bodyInput.value = ''; + if (ccInput) ccInput.value = ''; + if (bccInput) bccInput.value = ''; + if (attachmentSelect) { + Array.from(attachmentSelect.options).forEach((option) => { + option.selected = false; + }); + } + + statusEl.className = 'text-success'; + statusEl.textContent = 'E-mail sendt.'; + loadLinkedEmails(); + } catch (error) { + statusEl.className = 'text-danger'; + statusEl.textContent = error?.message || 'Kunne ikke sende e-mail.'; + } finally { + sendBtn.disabled = false; + } + } + function openCaseEmailTab() { const trigger = document.getElementById('emails-tab'); if (!trigger) return; @@ -6894,6 +7114,13 @@ // Load content on start document.addEventListener('DOMContentLoaded', () => { + const caseEmailSendBtn = document.getElementById('caseEmailSendBtn'); + if (caseEmailSendBtn) { + caseEmailSendBtn.addEventListener('click', sendCaseEmail); + } + + prefillCaseEmailCompose(); + updateCaseEmailAttachmentOptions(sagFilesCache); loadSagFiles(); loadLinkedEmails(); });