578 lines
25 KiB
JavaScript
578 lines
25 KiB
JavaScript
|
|
|
||
|
|
function _escapeCommentHtml(value) {
|
||
|
|
return String(value || '')
|
||
|
|
.replace(/&/g, '&')
|
||
|
|
.replace(/</g, '<')
|
||
|
|
.replace(/>/g, '>')
|
||
|
|
.replace(/"/g, '"')
|
||
|
|
.replace(/'/g, ''');
|
||
|
|
}
|
||
|
|
|
||
|
|
function _removeQuotedMailLines(text) {
|
||
|
|
const source = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||
|
|
const lines = source.split('\n');
|
||
|
|
const kept = [];
|
||
|
|
|
||
|
|
const headerRe = /^(fra|from|sent|date|dato|to|til|emne|subject|cc):\s*/i;
|
||
|
|
const originalMessageRe = /^(original message|oprindelig besked|videresendt besked)/i;
|
||
|
|
|
||
|
|
for (let i = 0; i < lines.length; i += 1) {
|
||
|
|
const line = lines[i];
|
||
|
|
const trimmed = line.trim();
|
||
|
|
|
||
|
|
if (trimmed.startsWith('>')) break;
|
||
|
|
if (originalMessageRe.test(trimmed)) break;
|
||
|
|
|
||
|
|
if (/^[-_]{3,}$/.test(trimmed)) {
|
||
|
|
const lookahead = lines.slice(i + 1, i + 4);
|
||
|
|
if (lookahead.some((candidate) => headerRe.test(String(candidate || '').trim()))) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (i > 0 && headerRe.test(trimmed) && String(lines[i - 1] || '').trim() === '') {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
kept.push(line);
|
||
|
|
}
|
||
|
|
|
||
|
|
while (kept.length > 0 && String(kept[kept.length - 1] || '').trim() === '') {
|
||
|
|
kept.pop();
|
||
|
|
}
|
||
|
|
|
||
|
|
return kept.join('\n').trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
function _parseEmailComment(rawText) {
|
||
|
|
const normalized = String(rawText || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||
|
|
const emailIdMatch = normalized.match(/^Email-ID:\s*(\d+)\s*$/m);
|
||
|
|
const emailId = emailIdMatch ? Number(emailIdMatch[1]) : null;
|
||
|
|
const withoutMeta = normalized.replace(/^Email-ID:\s*\d+\s*\n?/m, '').trim();
|
||
|
|
return {
|
||
|
|
emailId,
|
||
|
|
visibleText: _removeQuotedMailLines(withoutMeta)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function _formatEmailHeaderTimestamp(value) {
|
||
|
|
if (!value) return '';
|
||
|
|
const parsed = new Date(value);
|
||
|
|
if (Number.isNaN(parsed.getTime())) return String(value);
|
||
|
|
return parsed.toLocaleString('da-DK');
|
||
|
|
}
|
||
|
|
|
||
|
|
function _buildEmailHeaderAndBody(visibleText) {
|
||
|
|
const text = String(visibleText || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
||
|
|
const lines = text.split('\n');
|
||
|
|
|
||
|
|
let idx = 0;
|
||
|
|
let typeLabel = 'Indgaaende email';
|
||
|
|
const firstLine = String(lines[0] || '').trim();
|
||
|
|
if (/^📧\s*Indgående email/i.test(firstLine)) {
|
||
|
|
typeLabel = 'Indgaaende email';
|
||
|
|
idx = 1;
|
||
|
|
} else if (/^📧\s*Udgående email/i.test(firstLine)) {
|
||
|
|
typeLabel = 'Udgaaende email';
|
||
|
|
idx = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
let fra = '';
|
||
|
|
let til = '';
|
||
|
|
let cc = '';
|
||
|
|
let emne = '';
|
||
|
|
let modtaget = '';
|
||
|
|
|
||
|
|
while (idx < lines.length) {
|
||
|
|
const line = String(lines[idx] || '').trim();
|
||
|
|
if (!line) {
|
||
|
|
idx += 1;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (/^Fra:\s*/i.test(line)) fra = line.replace(/^Fra:\s*/i, '').trim();
|
||
|
|
else if (/^Til:\s*/i.test(line)) til = line.replace(/^Til:\s*/i, '').trim();
|
||
|
|
else if (/^Cc:\s*/i.test(line)) cc = line.replace(/^Cc:\s*/i, '').trim();
|
||
|
|
else if (/^Emne:\s*/i.test(line)) emne = line.replace(/^Emne:\s*/i, '').trim();
|
||
|
|
else if (/^Modtaget:\s*/i.test(line)) modtaget = line.replace(/^Modtaget:\s*/i, '').trim();
|
||
|
|
else break;
|
||
|
|
idx += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const bodyText = lines.slice(idx).join('\n').trim();
|
||
|
|
const summaryParts = [typeLabel];
|
||
|
|
if (fra) summaryParts.push(`Fra: ${fra}`);
|
||
|
|
if (til) summaryParts.push(`Til: ${til}`);
|
||
|
|
if (cc) summaryParts.push(`Cc: ${cc}`);
|
||
|
|
if (emne) summaryParts.push(`Emne: ${emne}`);
|
||
|
|
if (modtaget) summaryParts.push(`Modtaget: ${_formatEmailHeaderTimestamp(modtaget)}`);
|
||
|
|
|
||
|
|
return {
|
||
|
|
summary: summaryParts.join(' • '),
|
||
|
|
bodyText
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function _extractEmailHeaderFields(visibleText) {
|
||
|
|
const text = String(visibleText || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
||
|
|
const lines = text.split('\n');
|
||
|
|
let idx = 0;
|
||
|
|
|
||
|
|
const firstLine = String(lines[0] || '').trim();
|
||
|
|
const isOutgoing = /^📧\s*Udgående email/i.test(firstLine);
|
||
|
|
if (/^📧\s*(Indgående|Udgående)\s+email/i.test(firstLine)) {
|
||
|
|
idx = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
let fra = '';
|
||
|
|
let til = '';
|
||
|
|
let emne = '';
|
||
|
|
let modtaget = '';
|
||
|
|
|
||
|
|
while (idx < lines.length) {
|
||
|
|
const line = String(lines[idx] || '').trim();
|
||
|
|
if (!line) break;
|
||
|
|
if (/^Fra:\s*/i.test(line)) fra = line.replace(/^Fra:\s*/i, '').trim();
|
||
|
|
else if (/^Til:\s*/i.test(line)) til = line.replace(/^Til:\s*/i, '').trim();
|
||
|
|
else if (/^Emne:\s*/i.test(line)) emne = line.replace(/^Emne:\s*/i, '').trim();
|
||
|
|
else if (/^Modtaget:\s*/i.test(line)) modtaget = line.replace(/^Modtaget:\s*/i, '').trim();
|
||
|
|
else break;
|
||
|
|
idx += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return { fra, til, emne, modtaget, isOutgoing };
|
||
|
|
}
|
||
|
|
|
||
|
|
function _normalizeReplySubject(value) {
|
||
|
|
const subject = String(value || '').trim();
|
||
|
|
return subject.replace(/^(re|fw|fwd)\s*:\s*/ig, '').toLowerCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
function _findBestLinkedEmailByHeader(header) {
|
||
|
|
const targetSubject = _normalizeReplySubject(header?.emne || '');
|
||
|
|
const targetFrom = String(header?.fra || '').trim().toLowerCase();
|
||
|
|
const targetTo = String(header?.til || '').trim().toLowerCase();
|
||
|
|
|
||
|
|
const candidates = (linkedEmailsCache || []).filter((email) => {
|
||
|
|
const emailSubject = _normalizeReplySubject(email?.subject || '');
|
||
|
|
if (targetSubject && emailSubject !== targetSubject) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const sender = String(email?.sender_email || email?.sender_name || '').toLowerCase();
|
||
|
|
const recipient = String(email?.recipient_email || '').toLowerCase();
|
||
|
|
|
||
|
|
if (targetFrom && sender && sender.includes(targetFrom)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (targetTo && recipient && recipient.includes(targetTo)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return !targetFrom && !targetTo;
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!candidates.length) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
candidates.sort((a, b) => {
|
||
|
|
const aTs = a?.received_date ? new Date(a.received_date).getTime() : 0;
|
||
|
|
const bTs = b?.received_date ? new Date(b.received_date).getTime() : 0;
|
||
|
|
return bTs - aTs;
|
||
|
|
});
|
||
|
|
|
||
|
|
return Number(candidates[0]?.id) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function _extractEmailAddress(value) {
|
||
|
|
const raw = String(value || '').trim();
|
||
|
|
const match = raw.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
|
||
|
|
return match ? match[0] : raw;
|
||
|
|
}
|
||
|
|
|
||
|
|
function _commentInitials(name) {
|
||
|
|
const clean = String(name || '').trim();
|
||
|
|
if (!clean) return 'EM';
|
||
|
|
const parts = clean.split(/\s+/).filter(Boolean);
|
||
|
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||
|
|
return `${parts[0][0] || ''}${parts[1][0] || ''}`.toUpperCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
function _formatCommentTime(value) {
|
||
|
|
const parsed = new Date(value || Date.now());
|
||
|
|
if (Number.isNaN(parsed.getTime())) return '';
|
||
|
|
const pad = (n) => String(n).padStart(2, '0');
|
||
|
|
return `${pad(parsed.getDate())}/${pad(parsed.getMonth() + 1)}-${parsed.getFullYear()} ${pad(parsed.getHours())}:${pad(parsed.getMinutes())}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function _refreshCommentCountBadge() {
|
||
|
|
const container = document.getElementById('comments-container');
|
||
|
|
const badge = document.querySelector('#beskrivelse-comments-wrap .badge.bg-secondary');
|
||
|
|
if (!container || !badge) return;
|
||
|
|
badge.textContent = String(container.querySelectorAll('.comment-item').length);
|
||
|
|
}
|
||
|
|
|
||
|
|
function prependCommentToThread(comment) {
|
||
|
|
const container = document.getElementById('comments-container');
|
||
|
|
if (!container || !comment || !comment.indhold) return;
|
||
|
|
|
||
|
|
const emptyState = container.querySelector('p.text-center.text-muted.my-3');
|
||
|
|
if (emptyState) emptyState.remove();
|
||
|
|
|
||
|
|
const author = String(comment.forfatter || 'Email Bot');
|
||
|
|
const createdAtIso = String(comment.created_at || new Date().toISOString());
|
||
|
|
const createdAtMs = new Date(createdAtIso).getTime();
|
||
|
|
const createdAtUnix = Number.isFinite(createdAtMs) ? Math.floor(createdAtMs / 1000) : Math.floor(Date.now() / 1000);
|
||
|
|
|
||
|
|
const item = document.createElement('div');
|
||
|
|
item.className = 'comment-item comment-system';
|
||
|
|
item.dataset.createdAt = String(createdAtUnix);
|
||
|
|
|
||
|
|
const meta = document.createElement('div');
|
||
|
|
meta.className = 'comment-meta';
|
||
|
|
meta.innerHTML = `
|
||
|
|
<span class="comment-avatar">${_escapeCommentHtml(_commentInitials(author))}</span>
|
||
|
|
<b>${_escapeCommentHtml(author)}</b>
|
||
|
|
<span class="comment-time">${_escapeCommentHtml(_formatCommentTime(createdAtIso))}</span>
|
||
|
|
`;
|
||
|
|
|
||
|
|
const body = document.createElement('div');
|
||
|
|
body.className = 'comment-body';
|
||
|
|
body.setAttribute('data-comment-raw', String(comment.indhold));
|
||
|
|
body.textContent = String(comment.indhold);
|
||
|
|
|
||
|
|
item.appendChild(meta);
|
||
|
|
item.appendChild(body);
|
||
|
|
container.insertBefore(item, container.firstChild);
|
||
|
|
|
||
|
|
processCommentBodies();
|
||
|
|
sortCommentsNewestFirst();
|
||
|
|
_refreshCommentCountBadge();
|
||
|
|
}
|
||
|
|
|
||
|
|
let activeCommentQuickReply = null;
|
||
|
|
|
||
|
|
window.closeInlineCommentQuickReply = function() {
|
||
|
|
const host = document.getElementById('comment-quick-reply-host');
|
||
|
|
if (host) host.innerHTML = '';
|
||
|
|
activeCommentQuickReply = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
window.sendInlineCommentQuickReply = async function() {
|
||
|
|
const host = document.getElementById('comment-quick-reply-host');
|
||
|
|
const textarea = document.getElementById('commentQuickReplyText');
|
||
|
|
const sendBtn = document.getElementById('commentQuickReplySendBtn');
|
||
|
|
const statusEl = document.getElementById('commentQuickReplyStatus');
|
||
|
|
if (!host || !textarea || !sendBtn || !statusEl || !activeCommentQuickReply) return;
|
||
|
|
|
||
|
|
const bodyText = String(textarea.value || '').trim();
|
||
|
|
if (!bodyText) {
|
||
|
|
statusEl.className = 'comment-quick-reply-status text-danger';
|
||
|
|
statusEl.textContent = 'Skriv et svar';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const recipient = _extractEmailAddress(activeCommentQuickReply.recipient);
|
||
|
|
if (!recipient || recipient.indexOf('@') === -1) {
|
||
|
|
statusEl.className = 'comment-quick-reply-status text-danger';
|
||
|
|
statusEl.textContent = 'Ingen gyldig modtager fundet i kommentaren';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
sendBtn.disabled = true;
|
||
|
|
statusEl.className = 'comment-quick-reply-status';
|
||
|
|
statusEl.textContent = 'Sender...';
|
||
|
|
|
||
|
|
try {
|
||
|
|
await loadLinkedEmails();
|
||
|
|
|
||
|
|
let threadEmailId = Number(activeCommentQuickReply.emailId) || null;
|
||
|
|
if (!threadEmailId) {
|
||
|
|
threadEmailId = _findBestLinkedEmailByHeader(activeCommentQuickReply.header);
|
||
|
|
}
|
||
|
|
|
||
|
|
let threadKey = null;
|
||
|
|
if (threadEmailId) {
|
||
|
|
const linked = linkedEmailsCache.find((entry) => Number(entry.id) === Number(threadEmailId));
|
||
|
|
threadKey = linked?.thread_key || linked?.resolved_thread_key || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await fetch(`/api/v1/sag/${caseIds}/emails/send`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({
|
||
|
|
to: [recipient],
|
||
|
|
subject: activeCommentQuickReply.subject,
|
||
|
|
body_text: bodyText,
|
||
|
|
thread_email_id: threadEmailId,
|
||
|
|
thread_key: threadKey
|
||
|
|
})
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
let message = `HTTP ${response.status}`;
|
||
|
|
try {
|
||
|
|
const payload = await response.json();
|
||
|
|
message = payload?.detail || payload?.message || message;
|
||
|
|
} catch (_) {
|
||
|
|
}
|
||
|
|
throw new Error(message);
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
if (result?.comment) {
|
||
|
|
prependCommentToThread(result.comment);
|
||
|
|
}
|
||
|
|
|
||
|
|
statusEl.className = 'comment-quick-reply-status text-success';
|
||
|
|
statusEl.textContent = 'Svar sendt';
|
||
|
|
textarea.value = '';
|
||
|
|
await loadLinkedEmails();
|
||
|
|
setTimeout(() => {
|
||
|
|
window.closeInlineCommentQuickReply();
|
||
|
|
}, 500);
|
||
|
|
} catch (error) {
|
||
|
|
statusEl.className = 'comment-quick-reply-status text-danger';
|
||
|
|
statusEl.textContent = error?.message || 'Kunne ikke sende svar';
|
||
|
|
} finally {
|
||
|
|
sendBtn.disabled = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function openInlineCommentQuickReply(rawText, emailId) {
|
||
|
|
const host = document.getElementById('comment-quick-reply-host');
|
||
|
|
if (!host) return;
|
||
|
|
|
||
|
|
const parsed = _parseEmailComment(rawText || '');
|
||
|
|
const header = _extractEmailHeaderFields(parsed.visibleText || '');
|
||
|
|
const fallbackRecipient = header.isOutgoing ? (header.til || header.fra) : (header.fra || header.til);
|
||
|
|
const subject = /^re:\s*/i.test(header.emne || '')
|
||
|
|
? (header.emne || `Sag #${caseIds}`)
|
||
|
|
: `Re: ${header.emne || `Sag #${caseIds}`}`;
|
||
|
|
|
||
|
|
activeCommentQuickReply = {
|
||
|
|
rawText,
|
||
|
|
header,
|
||
|
|
emailId: Number(emailId) || parsed.emailId || null,
|
||
|
|
recipient: fallbackRecipient,
|
||
|
|
subject
|
||
|
|
};
|
||
|
|
|
||
|
|
host.innerHTML = `
|
||
|
|
<div class="comment-quick-reply-box">
|
||
|
|
<div class="small text-muted mb-1">Quick svar til ${_escapeCommentHtml(String(fallbackRecipient || 'ukendt modtager'))}</div>
|
||
|
|
<textarea id="commentQuickReplyText" class="form-control" rows="2" placeholder="Skriv hurtigt svar..."></textarea>
|
||
|
|
<div class="comment-quick-reply-actions">
|
||
|
|
<div id="commentQuickReplyStatus" class="comment-quick-reply-status"></div>
|
||
|
|
<div class="d-flex gap-2">
|
||
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="closeInlineCommentQuickReply()">Annuller</button>
|
||
|
|
<button type="button" class="btn btn-sm btn-primary" id="commentQuickReplySendBtn" onclick="sendInlineCommentQuickReply()"><i class="bi bi-send me-1"></i>Send</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
const textarea = document.getElementById('commentQuickReplyText');
|
||
|
|
if (textarea) {
|
||
|
|
textarea.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function quickReplyToEmailFromCommentText(rawText) {
|
||
|
|
openCaseEmailTab();
|
||
|
|
|
||
|
|
const parsed = _parseEmailComment(rawText || '');
|
||
|
|
const header = _extractEmailHeaderFields(parsed.visibleText || '');
|
||
|
|
|
||
|
|
try {
|
||
|
|
await loadLinkedEmails();
|
||
|
|
|
||
|
|
const matchedEmailId = _findBestLinkedEmailByHeader(header);
|
||
|
|
if (matchedEmailId) {
|
||
|
|
await loadLinkedEmailDetail(matchedEmailId);
|
||
|
|
openReplyToLinkedEmail();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Kunne ikke finde trådmail fra kommentar:', error);
|
||
|
|
}
|
||
|
|
|
||
|
|
const composeModalEl = document.getElementById('caseEmailComposeModal');
|
||
|
|
if (!composeModalEl) return;
|
||
|
|
|
||
|
|
const toInput = document.getElementById('caseEmailTo');
|
||
|
|
const subjectInput = document.getElementById('caseEmailSubject');
|
||
|
|
const bodyInput = document.getElementById('caseEmailBody');
|
||
|
|
|
||
|
|
const fallbackRecipient = (header.isOutgoing ? header.til : header.fra) || header.fra || header.til || '';
|
||
|
|
if (toInput && !toInput.value.trim() && fallbackRecipient) {
|
||
|
|
toInput.value = fallbackRecipient;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (subjectInput && !subjectInput.value.trim()) {
|
||
|
|
subjectInput.value = escapeHtmlForInput(
|
||
|
|
/^re:\s*/i.test(header.emne || '')
|
||
|
|
? (header.emne || `Sag #${caseIds}`)
|
||
|
|
: `Re: ${header.emne || `Sag #${caseIds}`}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bodyInput && !bodyInput.value.trim()) {
|
||
|
|
bodyInput.value = `\n\n---\nFra: ${header.fra || '-'}\nDato: ${header.modtaget || '-'}\nEmne: ${header.emne || '(Ingen emne)'}\n`;
|
||
|
|
}
|
||
|
|
|
||
|
|
bootstrap.Modal.getOrCreateInstance(composeModalEl).show();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function openEmailFromComment(emailId) {
|
||
|
|
const parsedId = Number(emailId);
|
||
|
|
if (!Number.isFinite(parsedId)) return;
|
||
|
|
|
||
|
|
if (typeof openCaseEmailTab === 'function') {
|
||
|
|
openCaseEmailTab();
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (typeof loadLinkedEmails === 'function') {
|
||
|
|
await loadLinkedEmails();
|
||
|
|
}
|
||
|
|
if (typeof loadLinkedEmailDetail === 'function') {
|
||
|
|
await loadLinkedEmailDetail(parsedId);
|
||
|
|
}
|
||
|
|
const emailTabPane = document.getElementById('emails');
|
||
|
|
if (emailTabPane) {
|
||
|
|
emailTabPane.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Kunne ikke åbne email fra kommentar:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function processCommentBodies() {
|
||
|
|
const commentItems = Array.from(document.querySelectorAll('#comments-container .comment-item'));
|
||
|
|
commentItems.forEach((item) => {
|
||
|
|
const body = item.querySelector('.comment-body');
|
||
|
|
if (!body) return;
|
||
|
|
|
||
|
|
const rawText = body.dataset.commentRaw || body.textContent || '';
|
||
|
|
if (!item.classList.contains('comment-system')) {
|
||
|
|
body.innerHTML = _escapeCommentHtml(String(rawText)).replace(/\n/g, '<br>');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const hasEmailHeader = /(^|\n)\s*📧\s*(Indgående|Udgående)\s+email/i.test(String(rawText));
|
||
|
|
if (!hasEmailHeader) {
|
||
|
|
body.innerHTML = _escapeCommentHtml(String(rawText)).replace(/\n/g, '<br>');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const parsed = _parseEmailComment(rawText);
|
||
|
|
const display = _buildEmailHeaderAndBody(parsed.visibleText || '');
|
||
|
|
const safeHeader = _escapeCommentHtml(display.summary || 'Indgaaende email');
|
||
|
|
const safeBody = _escapeCommentHtml(display.bodyText || '').replace(/\n/g, '<br>');
|
||
|
|
body.innerHTML = `
|
||
|
|
<div class="comment-email-header" title="${safeHeader}">${safeHeader}</div>
|
||
|
|
${display.bodyText ? `<div class="comment-email-text">${safeBody}</div>` : ''}
|
||
|
|
`;
|
||
|
|
|
||
|
|
const existingActions = item.querySelector('.comment-actions');
|
||
|
|
if (existingActions) {
|
||
|
|
existingActions.remove();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (parsed.emailId) {
|
||
|
|
const actions = document.createElement('div');
|
||
|
|
actions.className = 'comment-actions';
|
||
|
|
actions.innerHTML = `
|
||
|
|
<button type="button" class="btn btn-link btn-sm" onclick="openEmailFromComment(${parsed.emailId})"><i class="bi bi-envelope-open me-1"></i>Aabn fuld mail</button>
|
||
|
|
<button type="button" class="btn btn-link btn-sm" onclick="quickReplyToEmailFromComment(${parsed.emailId})"><i class="bi bi-reply me-1"></i>Svar</button>
|
||
|
|
<button type="button" class="btn btn-link btn-sm js-quick-inline-reply"><i class="bi bi-lightning-charge me-1"></i>Quick svar</button>
|
||
|
|
`;
|
||
|
|
item.appendChild(actions);
|
||
|
|
const quickInlineBtn = actions.querySelector('.js-quick-inline-reply');
|
||
|
|
if (quickInlineBtn) {
|
||
|
|
quickInlineBtn.addEventListener('click', () => {
|
||
|
|
openInlineCommentQuickReply(rawText, parsed.emailId);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const actions = document.createElement('div');
|
||
|
|
actions.className = 'comment-actions';
|
||
|
|
actions.innerHTML = `
|
||
|
|
<button type="button" class="btn btn-link btn-sm" onclick="openCaseEmailTab()"><i class="bi bi-envelope me-1"></i>Aabn email-fane</button>
|
||
|
|
<button type="button" class="btn btn-link btn-sm js-reply-fallback"><i class="bi bi-reply me-1"></i>Svar</button>
|
||
|
|
<button type="button" class="btn btn-link btn-sm js-quick-reply-fallback"><i class="bi bi-lightning-charge me-1"></i>Quick svar</button>
|
||
|
|
`;
|
||
|
|
item.appendChild(actions);
|
||
|
|
const replyBtn = actions.querySelector('.js-reply-fallback');
|
||
|
|
if (replyBtn) {
|
||
|
|
replyBtn.addEventListener('click', () => {
|
||
|
|
quickReplyToEmailFromCommentText(rawText);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const quickReplyBtn = actions.querySelector('.js-quick-reply-fallback');
|
||
|
|
if (quickReplyBtn) {
|
||
|
|
quickReplyBtn.addEventListener('click', () => {
|
||
|
|
openInlineCommentQuickReply(rawText, null);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function sortCommentsNewestFirst() {
|
||
|
|
const container = document.getElementById('comments-container');
|
||
|
|
if (!container) return;
|
||
|
|
|
||
|
|
const items = Array.from(container.querySelectorAll('.comment-item'));
|
||
|
|
if (items.length < 2) return;
|
||
|
|
|
||
|
|
items
|
||
|
|
.sort((a, b) => Number(b.dataset.createdAt || 0) - Number(a.dataset.createdAt || 0))
|
||
|
|
.forEach((item) => container.appendChild(item));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function submitComment(event) {
|
||
|
|
event.preventDefault();
|
||
|
|
const form = event.target;
|
||
|
|
const content = form.indhold.value;
|
||
|
|
const btn = form.querySelector('button');
|
||
|
|
const originalText = btn.innerHTML;
|
||
|
|
|
||
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Sender...';
|
||
|
|
btn.disabled = true;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/sag/{{ case.id }}/kommentarer', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({
|
||
|
|
indhold: content
|
||
|
|
})
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
location.reload();
|
||
|
|
} else {
|
||
|
|
alert('Fejl ved oprettelse af kommentar');
|
||
|
|
btn.innerHTML = originalText;
|
||
|
|
btn.disabled = false;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error:', error);
|
||
|
|
alert('Der skete en fejl. Prøv igen.');
|
||
|
|
btn.innerHTML = originalText;
|
||
|
|
btn.disabled = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Keep newest comments visible at top
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
sortCommentsNewestFirst();
|
||
|
|
processCommentBodies();
|
||
|
|
const container = document.getElementById('comments-container');
|
||
|
|
if(container) {
|
||
|
|
container.scrollTop = 0;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|