function _escapeCommentHtml(value) { return String(value || '') .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 = ` ${_escapeCommentHtml(_commentInitials(author))} ${_escapeCommentHtml(author)} ${_escapeCommentHtml(_formatCommentTime(createdAtIso))} `; 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 = `
`; 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, '