Fix: restore case email compose button in sag email tab
This commit is contained in:
parent
acdc94cd18
commit
959c9b4401
18
RELEASE_NOTES_v2.2.50.md
Normal file
18
RELEASE_NOTES_v2.2.50.md
Normal 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 prefill’er 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`
|
||||||
@ -3553,6 +3553,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body" id="emailDropZone">
|
<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="row g-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
@ -6346,32 +6384,70 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// FILES & EMAILS LOGIC
|
// FILES & EMAILS LOGIC
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
|
let sagFilesCache = [];
|
||||||
|
|
||||||
// ---------------- FILES ----------------
|
// ---------------- 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() {
|
async function loadSagFiles() {
|
||||||
const container = document.getElementById('files-list');
|
const container = document.getElementById('files-list');
|
||||||
if(!container) return;
|
if (container) {
|
||||||
container.innerHTML = '<div class="p-3 text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Henter filer...</div>';
|
container.innerHTML = '<div class="p-3 text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Henter filer...</div>';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/sag/${caseIds}/files`);
|
const res = await fetch(`/api/v1/sag/${caseIds}/files`);
|
||||||
if(res.ok) {
|
if(res.ok) {
|
||||||
const files = await res.json();
|
const files = await res.json();
|
||||||
|
sagFilesCache = Array.isArray(files) ? files : [];
|
||||||
|
updateCaseEmailAttachmentOptions(sagFilesCache);
|
||||||
renderFiles(files);
|
renderFiles(files);
|
||||||
} else {
|
} 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);
|
setModuleContentState('files', true);
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(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);
|
setModuleContentState('files', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFiles(files) {
|
function renderFiles(files) {
|
||||||
const container = document.getElementById('files-list');
|
const container = document.getElementById('files-list');
|
||||||
|
sagFilesCache = Array.isArray(files) ? files : [];
|
||||||
|
updateCaseEmailAttachmentOptions(sagFilesCache);
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(!files || files.length === 0) {
|
if(!files || files.length === 0) {
|
||||||
container.innerHTML = '<div class="p-3 text-center text-muted">Ingen filer fundet...</div>';
|
container.innerHTML = '<div class="p-3 text-center text-muted">Ingen filer fundet...</div>';
|
||||||
setModuleContentState('files', false);
|
setModuleContentState('files', false);
|
||||||
@ -6542,6 +6618,150 @@
|
|||||||
let linkedEmailsCache = [];
|
let linkedEmailsCache = [];
|
||||||
let selectedLinkedEmailId = null;
|
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, '"')
|
||||||
|
.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() {
|
function openCaseEmailTab() {
|
||||||
const trigger = document.getElementById('emails-tab');
|
const trigger = document.getElementById('emails-tab');
|
||||||
if (!trigger) return;
|
if (!trigger) return;
|
||||||
@ -6894,6 +7114,13 @@
|
|||||||
|
|
||||||
// Load content on start
|
// Load content on start
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const caseEmailSendBtn = document.getElementById('caseEmailSendBtn');
|
||||||
|
if (caseEmailSendBtn) {
|
||||||
|
caseEmailSendBtn.addEventListener('click', sendCaseEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
prefillCaseEmailCompose();
|
||||||
|
updateCaseEmailAttachmentOptions(sagFilesCache);
|
||||||
loadSagFiles();
|
loadSagFiles();
|
||||||
loadLinkedEmails();
|
loadLinkedEmails();
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user