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 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, '&')
|
||||
.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();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user