Fix: restore case email compose button in sag email tab

This commit is contained in:
Christian 2026-03-06 16:11:05 +01:00
parent acdc94cd18
commit 959c9b4401
3 changed files with 250 additions and 5 deletions

18
RELEASE_NOTES_v2.2.50.md Normal file
View File

@ -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 prefiller modtager (primær kontakt hvis muligt) og emne (`Sag #<id>:`).
- 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`

View File

@ -1 +1 @@
2.2.48
2.2.50

View File

@ -3553,6 +3553,44 @@
</div>
</div>
<div class="card-body" id="emailDropZone">
<div class="border rounded p-3 mb-3">
<div class="d-flex flex-column gap-2">
<div class="row g-2">
<div class="col-lg-6">
<label for="caseEmailTo" class="form-label form-label-sm mb-1">Til</label>
<input type="text" class="form-control form-control-sm" id="caseEmailTo" placeholder="modtager@eksempel.dk">
</div>
<div class="col-lg-3">
<label for="caseEmailCc" class="form-label form-label-sm mb-1">Cc</label>
<input type="text" class="form-control form-control-sm" id="caseEmailCc" placeholder="cc@eksempel.dk">
</div>
<div class="col-lg-3">
<label for="caseEmailBcc" class="form-label form-label-sm mb-1">Bcc</label>
<input type="text" class="form-control form-control-sm" id="caseEmailBcc" placeholder="bcc@eksempel.dk">
</div>
</div>
<div class="row g-2">
<div class="col-lg-8">
<label for="caseEmailSubject" class="form-label form-label-sm mb-1">Emne</label>
<input type="text" class="form-control form-control-sm" id="caseEmailSubject" placeholder="Emne">
</div>
<div class="col-lg-4">
<label for="caseEmailAttachmentIds" class="form-label form-label-sm mb-1">Vedhæft sagsfiler</label>
<select id="caseEmailAttachmentIds" class="form-select form-select-sm" multiple>
<option disabled>Ingen sagsfiler tilgængelige</option>
</select>
</div>
</div>
<div>
<label for="caseEmailBody" class="form-label form-label-sm mb-1">Besked</label>
<textarea class="form-control form-control-sm" id="caseEmailBody" rows="6" placeholder="Skriv besked..."></textarea>
</div>
<div class="d-flex justify-content-between align-items-center gap-2">
<small id="caseEmailSendStatus" class="text-muted"></small>
<button type="button" id="caseEmailSendBtn" class="btn btn-primary btn-sm">Ny email</button>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
<div class="row g-2">
@ -6347,31 +6385,69 @@
// 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 = '<option disabled>Ingen sagsfiler tilgængelige</option>';
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 `<option value="${fileId}">${filename} (${date})</option>`;
}).join('');
}
async function loadSagFiles() {
const container = document.getElementById('files-list');
if(!container) return;
container.innerHTML = '<div class="p-3 text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Henter filer...</div>';
if (container) {
container.innerHTML = '<div class="p-3 text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Henter filer...</div>';
}
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 = '<div class="p-3 text-center text-danger">Fejl ved hentning af filer</div>';
sagFilesCache = [];
updateCaseEmailAttachmentOptions(sagFilesCache);
if (container) {
container.innerHTML = '<div class="p-3 text-center text-danger">Fejl ved hentning af filer</div>';
}
setModuleContentState('files', true);
}
} catch(e) {
console.error(e);
container.innerHTML = '<div class="p-3 text-center text-danger">Fejl ved hentning af filer</div>';
sagFilesCache = [];
updateCaseEmailAttachmentOptions(sagFilesCache);
if (container) {
container.innerHTML = '<div class="p-3 text-center text-danger">Fejl ved hentning af filer</div>';
}
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 = '<div class="p-3 text-center text-muted">Ingen filer fundet...</div>';
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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();
});