2026-05-12 08:41:13 +02:00
{% extends "shared/frontend/base.html" %}
2026-07-28 14:18:24 +02:00
{% block title %}Email - BMC Hub{% endblock %}
2026-05-12 08:41:13 +02:00
{% block extra_css %}
< style >
.emails-v2-shell {
display: grid;
grid-template-columns: 340px minmax(0, 1fr) 420px;
gap: 1rem;
height: calc(100vh - 140px);
min-height: 620px;
}
.emails-v2-panel {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
display: flex;
flex-direction: column;
min-width: 0;
}
.emails-v2-header {
padding: 0.9rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.emails-v2-title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
}
.emails-v2-version-nav {
display: flex;
gap: 0.5rem;
}
.emails-v2-search {
padding: 0.75rem 0.9rem;
border-bottom: 1px solid var(--border-color);
}
2026-07-28 14:18:24 +02:00
.emails-quick-upload {
margin: 0.65rem 0.9rem;
padding: 0.55rem 0.7rem;
border: 1px dashed color-mix(in srgb, var(--accent, #0f4c75) 45%, var(--border-color));
border-radius: 9px;
background: color-mix(in srgb, var(--accent, #0f4c75) 4%, var(--bg-card));
color: var(--text-secondary);
cursor: pointer;
font-size: 0.8rem;
text-align: center;
transition: 0.15s ease;
}
.emails-quick-upload:hover,
.emails-quick-upload.dragover {
border-color: var(--accent);
color: var(--accent);
background: color-mix(in srgb, var(--accent, #0f4c75) 9%, var(--bg-card));
}
.emails-quick-upload.busy {
pointer-events: none;
opacity: 0.65;
}
.emails-primary-action {
border: 1px solid color-mix(in srgb, var(--accent, #0f4c75) 35%, var(--border-color));
background: color-mix(in srgb, var(--accent, #0f4c75) 5%, var(--bg-card));
}
2026-09-11 13:51:10 +02:00
.emails-v2-flow-picker {
width: 100%;
}
.emails-v2-flow-grid {
display: grid;
grid-template-columns: repeat(4, minmax(150px, 1fr));
gap: 0.65rem;
}
.emails-v2-flow-choice {
position: relative;
min-height: 74px;
padding: 0.72rem 0.8rem;
border: 1px solid var(--border-color);
border-radius: 10px;
background: #52606d;
color: #fff;
text-align: left;
box-shadow: 0 2px 5px rgba(15, 23, 42, 0.16);
}
.emails-v2-flow-choice:hover:not(:disabled) {
filter: brightness(1.08);
transform: translateY(-1px);
}
.emails-v2-flow-choice.recommended {
border: 3px solid #fff;
outline: 3px solid var(--accent);
}
.emails-v2-flow-choice.flow-support { background: #1769aa; }
.emails-v2-flow-choice.flow-invoice { background: #18864b; }
.emails-v2-flow-choice.flow-accounting { background: #d97706; }
.emails-v2-flow-choice.flow-archive { background: #59636e; }
.emails-v2-flow-choice:disabled {
opacity: 0.42;
box-shadow: none;
}
.emails-v2-flow-choice .flow-title {
display: block;
font-weight: 700;
}
.emails-v2-flow-choice .flow-description {
display: block;
margin-top: 0.22rem;
color: rgba(255, 255, 255, 0.88);
font-size: 0.76rem;
line-height: 1.25;
}
.emails-v2-flow-choice .flow-badge {
position: absolute;
top: -0.55rem;
right: 0.45rem;
padding: 0.12rem 0.42rem;
border-radius: 999px;
background: var(--accent);
color: #fff;
font-size: 0.66rem;
font-weight: 700;
}
@media (max-width: 1500px) {
.emails-v2-flow-grid {
grid-template-columns: repeat(2, minmax(150px, 1fr));
}
}
2026-07-30 20:11:43 +02:00
.email-ai-decision {
border: 2px solid color-mix(in srgb, var(--accent, #0f4c75) 42%, var(--border-color));
border-radius: 12px;
padding: 0.9rem;
margin-bottom: 0.85rem;
background: color-mix(in srgb, var(--accent, #0f4c75) 5%, var(--bg-card));
}
.email-ai-type {
font-size: 1.15rem;
font-weight: 750;
}
.email-ai-confidence {
height: 7px;
border-radius: 99px;
overflow: hidden;
background: var(--border-color);
}
.email-ai-confidence > span {
display: block;
height: 100%;
background: var(--accent);
}
.email-ai-actions {
margin: 0;
padding-left: 1.2rem;
font-size: 0.86rem;
}
.email-ai-actions li {
margin-bottom: 0.32rem;
}
2026-07-28 14:18:24 +02:00
.emails-shortcuts {
font-size: 0.72rem;
color: var(--text-secondary);
}
.emails-shortcuts kbd {
font-size: 0.68rem;
}
2026-05-12 08:41:13 +02:00
.emails-v2-filters {
padding: 0.65rem 0.9rem;
border-bottom: 1px solid var(--border-color);
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
justify-content: flex-end;
}
.emails-v2-filter {
border: 1px solid var(--border-color);
border-radius: 999px;
background: var(--bg-card);
color: var(--text-secondary);
font-size: 0.78rem;
font-weight: 600;
padding: 0.24rem 0.62rem;
}
.emails-v2-filter.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.emails-v2-list {
overflow: auto;
flex: 1;
}
.emails-v2-item {
border-bottom: 1px solid var(--border-color);
padding: 0.75rem 0.9rem;
cursor: pointer;
}
.emails-v2-item:hover {
background: color-mix(in srgb, var(--accent, #0f4c75) 6%, var(--bg-card));
}
.emails-v2-item.active {
background: color-mix(in srgb, var(--accent, #0f4c75) 14%, var(--bg-card));
border-left: 3px solid var(--accent);
padding-left: calc(0.9rem - 3px);
}
.emails-v2-item.unread .subject {
font-weight: 700;
}
.emails-v2-item .head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 0.5rem;
}
.emails-v2-item .sender {
font-size: 0.82rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.emails-v2-item .time {
font-size: 0.74rem;
color: var(--text-secondary);
white-space: nowrap;
}
.emails-v2-item .subject {
margin-top: 0.2rem;
font-size: 0.88rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.emails-v2-item .meta {
margin-top: 0.28rem;
font-size: 0.75rem;
color: var(--text-secondary);
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.emails-v2-empty {
padding: 2rem 1rem;
text-align: center;
color: var(--text-secondary);
}
.emails-v2-detail-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
}
.emails-v2-detail {
flex: 1;
overflow: auto;
padding: 1rem;
}
.emails-v2-mail-header {
padding: 0.9rem 1rem;
border-bottom: 1px solid var(--border-color);
background: color-mix(in srgb, var(--accent, #0f4c75) 5%, var(--bg-card));
}
2026-07-30 20:11:43 +02:00
.emails-v2-quick-toolbar {
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--border-color);
background: var(--bg-card);
}
.emails-v2-quick-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.45rem;
}
.emails-v2-quick-grid .btn {
min-height: 38px;
font-size: 0.82rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
2026-05-12 08:41:13 +02:00
.emails-v2-mail-body {
flex: 1;
overflow: auto;
padding: 1rem;
}
.emails-v2-actions-pane {
flex: 1;
overflow: auto;
padding: 1rem;
}
.emails-v2-subject {
font-size: 1.18rem;
font-weight: 700;
margin-bottom: 0.35rem;
word-break: break-word;
}
.emails-v2-topmeta {
color: var(--text-secondary);
font-size: 0.84rem;
margin-bottom: 0.9rem;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.emails-v2-actions {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin-bottom: 0.95rem;
justify-content: flex-start;
}
.emails-v2-right-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: flex-start;
}
.emails-v2-nextstep {
border: 1px dashed color-mix(in srgb, var(--accent, #0f4c75) 38%, transparent);
border-radius: 10px;
padding: 0.7rem;
margin-bottom: 0.9rem;
background: color-mix(in srgb, var(--accent, #0f4c75) 4%, var(--bg-card));
}
.emails-v2-nextstep .label {
font-size: 0.75rem;
font-weight: 700;
color: var(--text-secondary);
text-transform: uppercase;
margin-bottom: 0.3rem;
}
.emails-v2-card {
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 0.75rem;
margin-bottom: 0.75rem;
}
.emails-v2-card h6 {
margin: 0 0 0.5rem 0;
font-size: 0.82rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.02em;
}
.emails-v2-kv {
display: grid;
grid-template-columns: 130px 1fr;
gap: 0.35rem;
font-size: 0.82rem;
margin-bottom: 0.25rem;
}
.emails-v2-kv .k {
color: var(--text-secondary);
font-weight: 600;
}
.emails-v2-kv .v {
color: var(--text-primary);
overflow-wrap: anywhere;
}
.emails-v2-inline-status {
margin-top: 0.55rem;
font-size: 0.8rem;
color: var(--text-secondary);
}
.emails-v2-inline-status.error {
color: #b42318;
}
.emails-v2-inline-status.success {
color: #067647;
}
.emails-v2-body {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.35;
font-size: 0.92rem;
}
.emails-v2-body.html {
white-space: normal;
line-height: 1.5;
}
.emails-v2-body.html p,
.emails-v2-body.html ul,
.emails-v2-body.html ol,
.emails-v2-body.html blockquote,
.emails-v2-body.html pre,
.emails-v2-body.html table {
margin-bottom: 0.75rem;
}
.emails-v2-body.html table {
width: 100%;
border-collapse: collapse;
}
.emails-v2-body.html th,
.emails-v2-body.html td {
border: 1px solid var(--border-color);
padding: 0.35rem 0.45rem;
vertical-align: top;
}
.emails-v2-body.html a {
word-break: break-all;
}
.emails-v2-attachments {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.emails-v2-sag-results {
max-height: 220px;
overflow: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
}
.emails-v2-sag-result {
padding: 0.45rem 0.55rem;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
}
.emails-v2-sag-result:last-child {
border-bottom: none;
}
.emails-v2-sag-result:hover {
background: color-mix(in srgb, var(--accent, #0f4c75) 8%, var(--bg-card));
}
.emails-v2-status {
padding: 0.5rem 0.9rem;
border-top: 1px solid var(--border-color);
font-size: 0.8rem;
color: var(--text-secondary);
}
@media (max-width: 1100px) {
.emails-v2-shell {
grid-template-columns: 1fr;
height: auto;
min-height: 0;
}
.emails-v2-panel {
min-height: 420px;
}
}
< / style >
{% endblock %}
{% block content %}
< div class = "container-fluid py-3" >
< div class = "emails-v2-shell" >
< section class = "emails-v2-panel" >
< div class = "emails-v2-header" >
2026-07-28 14:18:24 +02:00
< h1 class = "emails-v2-title" > < i class = "bi bi-inbox me-2" > < / i > Email< / h1 >
< button id = "v2Refresh" class = "btn btn-sm btn-outline-primary" title = "Opdater (R)" >
< i class = "bi bi-arrow-clockwise" > < / i >
< / button >
2026-05-12 08:41:13 +02:00
< / div >
< div class = "emails-v2-search" >
2026-07-28 14:18:24 +02:00
< input id = "v2Search" class = "form-control form-control-sm" placeholder = "Søg i mails… (tryk /)" >
2026-05-12 08:41:13 +02:00
< / div >
< div id = "v2Filters" class = "emails-v2-filters" > < / div >
2026-07-28 14:18:24 +02:00
< div class = "emails-quick-upload" id = "v2UploadZone" role = "button" tabindex = "0" >
< span id = "v2UploadLabel" >
< i class = "bi bi-paperclip me-1" > < / i > Træk .eml/.msg hertil eller klik
< / span >
< input id = "v2UploadInput" type = "file" accept = ".eml,.msg" multiple hidden >
< / div >
2026-05-12 08:41:13 +02:00
< div id = "v2List" class = "emails-v2-list" > < / div >
< div id = "v2ListStatus" class = "emails-v2-status" > Klar< / div >
< / section >
< section class = "emails-v2-panel" >
< div class = "emails-v2-header" >
< h2 class = "emails-v2-title" > Detalje< / h2 >
2026-07-28 14:18:24 +02:00
< span class = "emails-shortcuts" > < kbd > J< / kbd > /< kbd > K< / kbd > næste/forrige< / span >
2026-05-12 08:41:13 +02:00
< / div >
< div id = "v2MailHeader" class = "emails-v2-mail-header small text-muted" > Vælg en email for at se info< / div >
2026-07-30 20:11:43 +02:00
< div id = "v2QuickActions" > < / div >
2026-05-12 08:41:13 +02:00
< div id = "v2MailBody" class = "emails-v2-detail-empty" > Vælg en email fra listen< / div >
< div id = "v2MailStatus" class = "emails-v2-status" > Ingen email valgt< / div >
< / section >
< section class = "emails-v2-panel" >
< div class = "emails-v2-header" >
< h2 class = "emails-v2-title" > Handlinger< / h2 >
2026-07-28 14:18:24 +02:00
< span class = "small text-muted" > Vælg hvad der skal ske< / span >
2026-05-12 08:41:13 +02:00
< / div >
< div id = "v2SideActions" class = "emails-v2-detail-empty" > Vælg en email for handlinger< / div >
< div id = "v2DetailStatus" class = "emails-v2-status" > Ingen email valgt< / div >
< / section >
< / div >
< / div >
{% endblock %}
{% block extra_js %}
< script >
(() => {
const FILTERS = [
{ key: 'active', label: 'Aktive' },
{ key: 'awaiting_user_action', label: 'Afventer handling' },
{ key: 'processed', label: 'Behandlede' },
{ key: 'all', label: 'Alle' },
];
const state = {
emails: [],
selectedEmail: null,
selectedEmailId: null,
workflowPreview: null,
filter: 'active',
query: '',
2026-07-28 14:18:24 +02:00
folder: '',
2026-05-12 08:41:13 +02:00
searchTimer: null,
sagSearchTimer: null,
vendorSuggestion: null,
domainCustomerSuggestion: null,
};
function escapeHtml(value) {
return String(value ?? '')
.replace(/& /g, '& ')
.replace(/< /g, '< ')
.replace(/>/g, '> ')
.replace(/"/g, '" ')
.replace(/'/g, '' ');
}
function formatEmailPlainText(value) {
return escapeHtml(value || '').replace(/\n/g, '< br > ');
}
function isSafeUrl(url) {
if (!url) return false;
const normalized = String(url).trim().toLowerCase();
return normalized.startsWith('http://')
|| normalized.startsWith('https://')
|| normalized.startsWith('mailto:')
|| normalized.startsWith('tel:')
|| normalized.startsWith('/');
}
function sanitizeEmailHtml(unsafeHtml) {
const input = String(unsafeHtml || '').trim();
if (!input) return '';
const allowedTags = new Set([
'a', 'b', 'strong', 'i', 'em', 'u', 's', 'br', 'p', 'div', 'span',
'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'hr',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'table', 'thead', 'tbody', 'tr', 'th', 'td'
]);
const allowedAttrs = {
a: new Set(['href', 'title', 'target', 'rel']),
th: new Set(['colspan', 'rowspan']),
td: new Set(['colspan', 'rowspan']),
};
const parser = new DOMParser();
const doc = parser.parseFromString(input, 'text/html');
const cleanNode = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
return document.createTextNode(node.textContent || '');
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return document.createTextNode('');
}
const tag = node.tagName.toLowerCase();
if (!allowedTags.has(tag)) {
const fragment = document.createDocumentFragment();
Array.from(node.childNodes).forEach((child) => {
fragment.appendChild(cleanNode(child));
});
return fragment;
}
const el = document.createElement(tag);
const tagAllowedAttrs = allowedAttrs[tag] || new Set();
Array.from(node.attributes).forEach((attr) => {
const name = attr.name.toLowerCase();
const value = attr.value || '';
if (!tagAllowedAttrs.has(name)) return;
if (tag === 'a' & & name === 'href') {
if (!isSafeUrl(value)) return;
el.setAttribute('href', value);
el.setAttribute('target', '_blank');
el.setAttribute('rel', 'noopener noreferrer');
return;
}
if ((name === 'colspan' || name === 'rowspan')) {
const num = Number(value);
if (!Number.isInteger(num) || num < 1 | | num > 100) return;
el.setAttribute(name, String(num));
return;
}
el.setAttribute(name, value);
});
Array.from(node.childNodes).forEach((child) => {
el.appendChild(cleanNode(child));
});
return el;
};
const wrapper = document.createElement('div');
Array.from(doc.body.childNodes).forEach((child) => {
wrapper.appendChild(cleanNode(child));
});
return wrapper.innerHTML;
}
function formatDate(value) {
if (!value) return '-';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return '-';
return d.toLocaleString('da-DK');
}
async function apiFetch(url, options = {}) {
const response = await fetch(url, { credentials: 'include', ...options });
if (!response.ok) {
let detail = `HTTP ${response.status}`;
try {
const payload = await response.json();
detail = payload?.detail || detail;
} catch (_) {
const text = await response.text().catch(() => '');
if (text) detail = text;
}
throw new Error(detail);
}
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
return response.text();
}
function setListStatus(message) {
const el = document.getElementById('v2ListStatus');
if (el) el.textContent = message || '';
}
function setDetailStatus(message) {
const el = document.getElementById('v2DetailStatus');
if (el) el.textContent = message || '';
}
function setMailStatus(message) {
const el = document.getElementById('v2MailStatus');
if (el) el.textContent = message || '';
}
function renderFilters() {
const host = document.getElementById('v2Filters');
if (!host) return;
const counts = {
active: state.emails.filter((e) => ['new', 'awaiting_user_action'].includes((e.status || '').toLowerCase())).length,
awaiting_user_action: state.emails.filter((e) => (e.status || '').toLowerCase() === 'awaiting_user_action').length,
processed: state.emails.filter((e) => (e.status || '').toLowerCase() === 'processed').length,
all: state.emails.length,
};
host.innerHTML = FILTERS.map((f) => {
const active = state.filter === f.key ? 'active' : '';
return `< button class = "emails-v2-filter ${active}" data-filter = "${f.key}" > ${escapeHtml(f.label)} (${counts[f.key] || 0})< / button > `;
}).join('');
host.querySelectorAll('[data-filter]').forEach((btn) => {
btn.addEventListener('click', () => {
state.filter = btn.getAttribute('data-filter') || 'active';
loadEmails();
});
});
}
function renderList() {
const host = document.getElementById('v2List');
if (!host) return;
if (!state.emails.length) {
host.innerHTML = '< div class = "emails-v2-empty" > Ingen emails matcher dit filter< / div > ';
return;
}
host.innerHTML = state.emails.map((email) => {
const active = Number(email.id) === Number(state.selectedEmailId) ? 'active' : '';
const unread = email.is_read ? '' : 'unread';
const sender = email.sender_name || email.sender_email || 'Ukendt';
const preview = (email.body_text || email.subject || '').slice(0, 90).replace(/\s+/g, ' ').trim();
return `
< article class = "emails-v2-item ${active} ${unread}" data-email-id = "${Number(email.id)}" >
< div class = "head" >
< div class = "sender" > ${escapeHtml(sender)}< / div >
< div class = "time" > ${escapeHtml(formatDate(email.received_date))}< / div >
< / div >
< div class = "subject" > ${escapeHtml(email.subject || '(Ingen emne)')}< / div >
< div class = "meta" >
< span > Status: ${escapeHtml(email.status || '-')}< / span >
< span > Type: ${escapeHtml(email.classification || 'general')}< / span >
${email.linked_case_id ? `< span > SAG #${Number(email.linked_case_id)}< / span > ` : ''}
< / div >
${preview ? `< div class = "meta" > ${escapeHtml(preview)}< / div > ` : ''}
< / article >
`;
}).join('');
host.querySelectorAll('[data-email-id]').forEach((node) => {
node.addEventListener('click', () => {
selectEmail(Number(node.getAttribute('data-email-id')));
});
});
}
async function loadEmails() {
setListStatus('Indlæser emails...');
const host = document.getElementById('v2List');
if (host) {
host.innerHTML = '< div class = "emails-v2-empty" > < div class = "spinner-border spinner-border-sm" > < / div > Henter...< / div > ';
}
let url = '/api/v1/emails?limit=150';
if (state.filter === 'awaiting_user_action') {
url += '&status=awaiting_user_action';
} else if (state.filter === 'processed') {
url += '&status=processed';
}
if (state.query) {
url += `&q=${encodeURIComponent(state.query)}`;
}
if (state.folder) {
url += `&folder=${encodeURIComponent(state.folder)}`;
}
try {
const rows = await apiFetch(url);
let emails = Array.isArray(rows) ? rows : [];
2026-07-30 20:11:43 +02:00
// Processed/archived emails must never reappear in the active
// inbox, including when the user searches.
if (state.filter === 'active') {
2026-05-12 08:41:13 +02:00
emails = emails.filter((e) => ['new', 'awaiting_user_action'].includes((e.status || '').toLowerCase()));
}
state.emails = emails;
renderFilters();
renderList();
2026-07-28 14:18:24 +02:00
setListStatus(`${emails.length} emails`);
2026-05-12 08:41:13 +02:00
if (state.selectedEmailId) {
const stillExists = emails.some((e) => Number(e.id) === Number(state.selectedEmailId));
if (stillExists) {
await selectEmail(state.selectedEmailId, { silentListRefresh: true });
2026-07-30 20:11:43 +02:00
} else {
state.selectedEmailId = null;
state.selectedEmail = null;
renderDetail(null);
if (emails.length) {
await selectEmail(Number(emails[0].id), { silentListRefresh: true });
}
2026-05-12 08:41:13 +02:00
}
2026-07-28 14:18:24 +02:00
} else if (emails.length) {
await selectEmail(Number(emails[0].id), { silentListRefresh: true });
2026-05-12 08:41:13 +02:00
}
} catch (error) {
state.emails = [];
renderFilters();
renderList();
setListStatus(`Fejl: ${error.message}`);
}
}
async function selectEmail(emailId, options = {}) {
if (!emailId) return;
state.selectedEmailId = Number(emailId);
renderList();
setDetailStatus('Indlæser email...');
try {
const email = await apiFetch(`/api/v1/emails/${emailId}`);
state.selectedEmail = email;
renderDetail(email);
setDetailStatus(`Email #${emailId} indlæst`);
if (!options.silentListRefresh) {
await loadEmails();
}
} catch (error) {
setDetailStatus(`Fejl: ${error.message}`);
}
}
async function patchReadState(isRead) {
if (!state.selectedEmailId) return;
try {
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/read-state`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_read: Boolean(isRead) }),
});
await selectEmail(state.selectedEmailId);
} catch (error) {
setDetailStatus(`Kunne ikke opdatere læsestatus: ${error.message}`);
}
}
async function archiveCurrent() {
if (!state.selectedEmailId) return;
try {
await apiFetch('/api/v1/emails/bulk/archive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([state.selectedEmailId]),
});
state.selectedEmailId = null;
state.selectedEmail = null;
renderDetail(null);
await loadEmails();
setDetailStatus('Email arkiveret');
} catch (error) {
setDetailStatus(`Kunne ikke arkivere email: ${error.message}`);
}
}
async function markProcessedCurrent() {
if (!state.selectedEmailId) return;
try {
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/mark-processed`, {
method: 'POST',
});
await selectEmail(state.selectedEmailId);
setDetailStatus('Email markeret som behandlet');
} catch (error) {
setDetailStatus(`Kunne ikke markere som behandlet: ${error.message}`);
}
}
async function reprocessCurrent() {
if (!state.selectedEmailId) return;
try {
setDetailStatus('Genbehandler email...');
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/reprocess`, {
method: 'POST',
});
await selectEmail(state.selectedEmailId);
setDetailStatus('Email genbehandlet');
} catch (error) {
setDetailStatus(`Kunne ikke genbehandle email: ${error.message}`);
}
}
async function executeWorkflowsCurrent() {
if (!state.selectedEmailId) return;
try {
setDetailStatus('Kører workflows...');
const result = await apiFetch(`/api/v1/emails/${state.selectedEmailId}/execute-workflows`, {
method: 'POST',
});
await selectEmail(state.selectedEmailId);
const executed = Number(result?.workflows_executed || result?.executed || 0);
setDetailStatus(`Workflows kørt (${executed})`);
} catch (error) {
setDetailStatus(`Kunne ikke køre workflows: ${error.message}`);
}
}
async function loadWorkflowPreviewCurrent() {
if (!state.selectedEmailId) return;
try {
const preview = await apiFetch(`/api/v1/emails/${state.selectedEmailId}/workflow-preview`);
state.workflowPreview = preview || null;
2026-07-30 20:11:43 +02:00
if (preview?.identity) {
state.domainCustomerSuggestion = preview.identity.customer || {
domain: String(state.selectedEmail?.sender_email || '').split('@')[1] || null,
suggestion: null,
};
state.vendorSuggestion = preview.identity.vendor?.suggestion || {};
renderDomainCustomerSuggestion();
renderVendorSuggestion();
}
2026-05-12 08:41:13 +02:00
renderWorkflowPreview();
} catch (error) {
state.workflowPreview = null;
renderWorkflowPreview(`Kunne ikke hente workflow-match: ${error.message}`);
}
}
async function autoRunWorkflowsCurrent() {
if (!state.selectedEmailId) return;
try {
setDetailStatus('Kører autokør...');
const result = await apiFetch(`/api/v1/emails/${state.selectedEmailId}/auto-run-workflows`, {
method: 'POST',
});
await selectEmail(state.selectedEmailId);
const executed = Number(result?.workflows_executed || result?.executed || 0);
setDetailStatus(`Autokør fuldført (${executed})`);
} catch (error) {
setDetailStatus(`Autokør fejlede: ${error.message}`);
}
}
2026-07-30 20:11:43 +02:00
const CLASSIFICATION_LABELS = {
invoice: 'Leverandørfaktura',
freight_note: 'Fragtbrev',
order_confirmation: 'Ordrebekræftelse',
time_confirmation: 'Tidsbekræftelse',
case_notification: 'Sagsnotifikation',
customer_email: 'Kundehenvendelse',
bankruptcy: 'Konkursmeddelelse',
general: 'Almindelig email',
spam: 'Spam',
newsletter: 'Nyhedsbrev',
unknown: 'Ukendt type',
};
const WORKFLOW_ACTION_LABELS = {
create_ticket: 'Opretter en sag/opgave',
link_email_to_ticket: 'Knytter emailen til en eksisterende sag',
route_helpdesk_sag: 'Finder eller opretter den relevante sag',
create_time_entry: 'Opretter en tidsregistrering',
link_to_vendor: 'Knytter emailen til leverandøren',
link_to_customer: 'Knytter emailen til kunden',
extract_invoice_data: 'Sender PDF-bilaget til Leverandørfakturaer → Mangler behandling',
extract_tracking_number: 'Udtrækker trackingnummer',
regex_extract_and_link: 'Finder reference og opretter tilknytning',
process_also_cloud_billing: 'Importerer ALSO-fakturafilen og opretter ordrekladder',
send_slack_notification: 'Sender en Slack-notifikation',
send_email_notification: 'Sender en emailnotifikation',
mark_as_processed: 'Markerer emailen som behandlet',
flag_for_review: 'Markerer emailen til manuel kontrol',
};
function renderIdentityAssessment() {
const email = state.selectedEmail || {};
const customerData = state.domainCustomerSuggestion;
const customerSuggestion = customerData?.suggestion;
const vendorSuggestion = state.vendorSuggestion;
const senderDomain = customerData?.domain
|| String(email.sender_email || '').split('@')[1]
|| '';
let customerHtml;
if (customerData?.not_applicable) {
customerHtml = `
< div class = "fw-semibold" > Ikke relevant< / div >
< div class = "small text-muted" > Dette er et leverandørdokument. Afsenderen matches kun mod leverandører.< / div >
`;
} else if (email.customer_id) {
customerHtml = `
< div class = "fw-semibold text-success" > ${escapeHtml(email.customer_name || `Kunde #${email.customer_id}`)}< / div >
< div class = "small text-muted" > Allerede tilknyttet denne email< / div >
`;
} else if (customerSuggestion?.customer_id) {
customerHtml = `
< div class = "fw-semibold" > ${escapeHtml(customerSuggestion.customer_name || `Kunde #${customerSuggestion.customer_id}`)}< / div >
< div class = "small text-muted" > ${escapeHtml(customerSuggestion.confidence || 'ukendt')} sikkerhed · ${escapeHtml(customerSuggestion.source || 'domænematch')}< / div >
< button id = "v2DecisionLinkCustomer" class = "btn btn-sm btn-outline-primary mt-2" > Tilknyt kunden< / button >
`;
} else if (customerData) {
const proposedName = email.sender_name || senderDomain || 'Ny kunde';
customerHtml = `
< div class = "fw-semibold" > ${escapeHtml(proposedName)}< / div >
< div class = "small text-muted" > Ingen eksisterende kunde fundet${senderDomain ? ` for ${escapeHtml(senderDomain)}` : ''}< / div >
< button id = "v2DecisionCreateCustomer" class = "btn btn-sm btn-primary mt-2" >
< i class = "bi bi-plus-lg me-1" > < / i > Opret og tilknyt kunde
< / button >
`;
} else {
customerHtml = '< div class = "small text-muted" > < span class = "spinner-border spinner-border-sm me-1" > < / span > Finder kunde…< / div > ';
}
let vendorHtml;
if (email.supplier_id) {
vendorHtml = `
< div class = "fw-semibold text-success" > ${escapeHtml(email.supplier_name || `Leverandør #${email.supplier_id}`)}< / div >
< div class = "small text-muted" > Allerede tilknyttet denne email< / div >
`;
} else if (vendorSuggestion?.vendor_id) {
vendorHtml = `
< div class = "fw-semibold" > ${escapeHtml(vendorSuggestion.name || `Leverandør #${vendorSuggestion.vendor_id}`)}< / div >
< div class = "small text-muted" > Matchscore ${Number(vendorSuggestion.match_score || 0)}${vendorSuggestion.cvr_number ? ` · CVR ${escapeHtml(vendorSuggestion.cvr_number)}` : ''}< / div >
< button id = "v2DecisionLinkVendor" class = "btn btn-sm btn-outline-primary mt-2" > Tilknyt leverandøren< / button >
`;
} else if (vendorSuggestion?.name) {
vendorHtml = `
< div class = "fw-semibold" > ${escapeHtml(vendorSuggestion.name)}< / div >
< div class = "small text-muted" > Ny leverandør${vendorSuggestion.cvr_number ? ` · CVR ${escapeHtml(vendorSuggestion.cvr_number)}` : ''}< / div >
< button id = "v2DecisionCreateVendor" class = "btn btn-sm btn-primary mt-2" >
< i class = "bi bi-plus-lg me-1" > < / i > Opret og tilknyt leverandør
< / button >
`;
} else if (vendorSuggestion) {
vendorHtml = `
< div class = "small text-muted" > Systemet fandt ikke en sikker leverandør.< / div >
< button id = "v2DecisionCreateVendor" class = "btn btn-sm btn-outline-primary mt-2" >
Opret leverandør fra afsender
< / button >
`;
} else {
vendorHtml = '< div class = "small text-muted" > < span class = "spinner-border spinner-border-sm me-1" > < / span > Finder leverandør…< / div > ';
}
return `
< div class = "row g-2 mb-3" >
< div class = "col-12" >
< div class = "border rounded p-2" >
< div class = "small text-uppercase fw-bold text-muted mb-1" > < i class = "bi bi-building me-1" > < / i > Systemet tror kunden er< / div >
${customerHtml}
< / div >
< / div >
< div class = "col-12" >
< div class = "border rounded p-2" >
< div class = "small text-uppercase fw-bold text-muted mb-1" > < i class = "bi bi-truck me-1" > < / i > Systemet tror leverandøren er< / div >
${vendorHtml}
< / div >
< / div >
< / div >
`;
}
function bindIdentityDecisionActions() {
document.getElementById('v2DecisionLinkCustomer')?.addEventListener('click', applyDomainCustomerSuggestion);
document.getElementById('v2DecisionCreateCustomer')?.addEventListener('click', createCustomerFromSuggestion);
document.getElementById('v2DecisionLinkVendor')?.addEventListener('click', linkVendorSuggestion);
document.getElementById('v2DecisionCreateVendor')?.addEventListener('click', createVendorFromSuggestion);
}
function renderClassificationDecision(errorMessage) {
const host = document.getElementById('v2DecisionCard');
if (!host) return;
if (errorMessage) {
host.innerHTML = `
< div class = "fw-semibold text-danger" > Vurdering kunne ikke indlæses< / div >
< div class = "small text-muted mt-1" > ${escapeHtml(errorMessage)}< / div >
`;
return;
}
const preview = state.workflowPreview;
if (!preview) {
host.innerHTML = '< div class = "small text-muted" > < span class = "spinner-border spinner-border-sm me-2" > < / span > Systemet vurderer mailen…< / div > ';
return;
}
const meta = preview.email || {};
const classification = String(meta.classification || 'unknown').toLowerCase();
const typeLabel = CLASSIFICATION_LABELS[classification] || classification;
const rawConfidence = Number(meta.confidence_score || 0);
const confidencePercent = Math.round(Math.max(0, Math.min(1, rawConfidence)) * 100);
const confidenceLabel = confidencePercent >= 80
? 'Høj sikkerhed'
: confidencePercent >= 55 ? 'Middel sikkerhed' : 'Lav sikkerhed – kontrollér manuelt';
2026-09-11 13:51:10 +02:00
const email = state.selectedEmail || {};
const isApprovedSupplierInvoice = classification === 'invoice' & & Boolean(email.supplier_id);
2026-07-30 20:11:43 +02:00
const effects = [];
2026-09-11 13:51:10 +02:00
if (isApprovedSupplierInvoice) {
effects.push(
'Beholder tilknytningen til den valgte leverandør',
'Henter PDF-bilaget og sender det til Leverandørfakturaer',
'Aflæser fakturanummer, beløb og varelinjer',
'Opdaterer Internetforbindelser automatisk, når fakturaen er fra GlobalConnect',
'Markerer emailen som behandlet'
);
} else {
(preview.system_matches || []).filter((row) => row.matches).forEach((row) => {
if (row.effect) effects.push(row.effect);
});
(preview.matching_workflows || []).forEach((workflow) => {
const actions = Array.isArray(workflow.actions) ? workflow.actions : [];
actions.forEach((action) => effects.push(
WORKFLOW_ACTION_LABELS[action] || `Kører handlingen “${action}”`
));
});
}
2026-07-30 20:11:43 +02:00
const uniqueEffects = [...new Set(effects)];
const hasActions = uniqueEffects.length > 0;
const automaticExecution = Boolean(preview.automatic_execution);
host.innerHTML = `
2026-09-11 13:51:10 +02:00
< div class = "small text-uppercase fw-bold text-muted mb-1" > Anbefalet behandling< / div >
< div class = "d-flex justify-content-between align-items-center gap-2" >
< div class = "email-ai-type" > ${escapeHtml(typeLabel)}< / div >
< span class = "badge ${confidencePercent >= 80 ? 'bg-success' : confidencePercent >= 55 ? 'bg-warning text-dark' : 'bg-secondary'}" > ${confidencePercent}%< / span >
2026-07-30 20:11:43 +02:00
< / div >
2026-09-11 13:51:10 +02:00
< div class = "small text-muted mt-1" > ${escapeHtml(confidenceLabel)}. Vælg flowet øverst på siden.< / div >
< div class = "mt-3" >
< div class = "small fw-semibold mb-2" > Vurdering og handlinger< / div >
< div class = "mt-3" >
${renderIdentityAssessment()}
< div class = "fw-semibold small mb-2" > ${automaticExecution ? 'Dette konkursflow køres automatisk:' : 'Flowet udfører:'}< / div >
${hasActions ? `
< ol class = "email-ai-actions mb-2" >
${uniqueEffects.map((effect) => `< li > ${escapeHtml(effect)}< / li > `).join('')}
< / ol >
${automaticExecution ? `
2026-07-30 20:11:43 +02:00
< div class = "alert alert-danger py-2 small mb-0" >
< i class = "bi bi-lightning-charge-fill me-1" > < / i >
Automatisk sikkerhedsflow. Der handles kun ved eksakt CVR-match.
2026-09-11 13:51:10 +02:00
< / div > ` : ''}
` : '< div class = "small text-muted" > Ingen automatiske handlinger foreslås.< / div > '}
2026-07-30 20:11:43 +02:00
< / div >
< / div >
`;
bindIdentityDecisionActions();
}
async function approveSuggestedActions() {
const button = document.getElementById('v2ApproveActions');
if (!button || !state.selectedEmailId) return;
button.disabled = true;
button.innerHTML = '< span class = "spinner-border spinner-border-sm me-2" > < / span > Udfører…';
try {
2026-09-11 13:51:10 +02:00
const classification = String(state.workflowPreview?.email?.classification || '').toLowerCase();
if (classification === 'invoice' & & state.selectedEmail?.supplier_id) {
await runQuickAction('create_supplier_invoice', button);
} else {
await executeWorkflowsCurrent();
}
2026-07-30 20:11:43 +02:00
} finally {
if (document.body.contains(button)) {
button.disabled = false;
button.innerHTML = '< i class = "bi bi-check-circle me-2" > < / i > Godkend og udfør';
}
}
}
2026-05-12 08:41:13 +02:00
function renderWorkflowPreview(errorMessage) {
const host = document.getElementById('v2WorkflowPreview');
if (!host) return;
if (errorMessage) {
host.innerHTML = `< div class = "small text-danger" > ${escapeHtml(errorMessage)}< / div > `;
2026-07-30 20:11:43 +02:00
renderClassificationDecision(errorMessage);
2026-05-12 08:41:13 +02:00
return;
}
const preview = state.workflowPreview;
if (!preview) {
host.innerHTML = '< div class = "small text-muted" > Ingen preview endnu< / div > ';
2026-07-30 20:11:43 +02:00
renderClassificationDecision();
2026-05-12 08:41:13 +02:00
return;
}
const matching = Array.isArray(preview.matching_workflows) ? preview.matching_workflows : [];
const system = Array.isArray(preview.system_matches) ? preview.system_matches : [];
const emailMeta = preview.email || {};
2026-09-11 13:51:10 +02:00
const usesSupplierApprovalFlow = String(emailMeta.classification || '').toLowerCase() === 'invoice'
& & Boolean(state.selectedEmail?.supplier_id);
2026-05-12 08:41:13 +02:00
const systemHtml = system.map((row) => {
const badge = row.matches ? '< span class = "badge bg-success" > Matcher< / span > ' : '< span class = "badge bg-secondary" > Springes over< / span > ';
return `
< div class = "emails-v2-kv" >
< div class = "k" > ${escapeHtml(row.name || row.code || 'System')}< / div >
< div class = "v" > ${badge} < span class = "small text-muted" > ${escapeHtml(row.reason || '')}< / span > < / div >
< / div >
`;
}).join('');
2026-09-11 13:51:10 +02:00
const matchingHtml = usesSupplierApprovalFlow
? `< div class = "emails-v2-kv" >
< div class = "k" > Godkendelsesflow< / div >
< div class = "v" >
< span class = "badge bg-success" > Leverandørfaktura< / span >
< span class = "small text-muted" > PDF → fakturaudtræk → evt. GlobalConnect Internetforbindelser → behandlet< / span >
< / div >
< / div > `
: matching.length
2026-05-12 08:41:13 +02:00
? matching.map((wf) => `
< div class = "emails-v2-kv" >
< div class = "k" > #${Number(wf.id)} ${escapeHtml(wf.name || '')}< / div >
< div class = "v" >
< span class = "badge bg-success" > Matcher< / span >
< span class = "small text-muted" > prio ${escapeHtml(String(wf.priority ?? '-'))} • min conf ${escapeHtml(String(wf.confidence_threshold ?? '-'))} • steps ${escapeHtml(String(wf.steps_total ?? 0))}< / span >
< / div >
< / div >
`).join('')
: '< div class = "small text-muted" > Ingen bruger-workflows matcher aktuelt.< / div > ';
host.innerHTML = `
< div class = "emails-v2-kv" > < div class = "k" > Klassifikation< / div > < div class = "v" > ${escapeHtml(emailMeta.classification || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Confidence< / div > < div class = "v" > ${escapeHtml(String(emailMeta.confidence_score ?? 0))}< / div > < / div >
< div class = "mt-2" >
< div class = "small fw-semibold mb-1" > Systemflows< / div >
${systemHtml || '< div class = "small text-muted" > Ingen systemflows< / div > '}
< / div >
< div class = "mt-2" >
< div class = "small fw-semibold mb-1" > Workflow match< / div >
${matchingHtml}
< / div >
`;
2026-07-30 20:11:43 +02:00
renderClassificationDecision();
2026-05-12 08:41:13 +02:00
const autoRunBtn = document.getElementById('v2AutoRun');
if (autoRunBtn) {
const enabled = Boolean(preview.auto_run_enabled);
autoRunBtn.disabled = !enabled;
autoRunBtn.title = enabled
? 'Autokør er aktiv'
: 'Aktiveres senere via EMAIL_WORKFLOW_AUTORUN_ENABLED=true';
}
}
async function autoParseAndLinkCurrent() {
if (!state.selectedEmailId) return;
try {
setDetailStatus('Auto parser email til tråd/sag...');
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/execute-workflows`, {
method: 'POST',
});
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/reprocess`, {
method: 'POST',
});
await selectEmail(state.selectedEmailId);
setDetailStatus('Auto parse gennemført');
} catch (error) {
setDetailStatus(`Auto parse fejlede: ${error.message}`);
}
}
async function createCaseFromCurrent() {
if (!state.selectedEmailId || !state.selectedEmail) return;
const caseTypeEl = document.getElementById('v2CaseType');
const titelEl = document.getElementById('v2CaseTitle');
2026-07-30 20:11:43 +02:00
const createButton = document.getElementById('v2CreateCase');
2026-05-12 08:41:13 +02:00
const payload = {
titel: String(titelEl?.value || state.selectedEmail.subject || '').trim(),
case_type: String(caseTypeEl?.value || 'support'),
};
if (state.selectedEmail.customer_id) {
payload.customer_id = Number(state.selectedEmail.customer_id);
}
try {
2026-07-30 20:11:43 +02:00
if (createButton) {
createButton.disabled = true;
createButton.innerHTML = '< span class = "spinner-border spinner-border-sm me-1" > < / span > Opretter…';
}
2026-05-12 08:41:13 +02:00
const result = await apiFetch(`/api/v1/emails/${state.selectedEmailId}/create-sag`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const sagId = Number(result?.sag?.id || result?.sag_id || 0);
setDetailStatus(sagId ? `SAG #${sagId} oprettet` : 'SAG oprettet');
await selectEmail(state.selectedEmailId);
} catch (error) {
setDetailStatus(`Kunne ikke oprette sag: ${error.message}`);
2026-07-30 20:11:43 +02:00
window.alert(`Sagen kunne ikke oprettes:\n${error.message}`);
} finally {
if (createButton & & document.body.contains(createButton)) {
createButton.disabled = false;
createButton.innerHTML = '< i class = "bi bi-plus-lg me-1" > < / i > Opret';
}
}
}
async function runQuickAction(action, button) {
if (!state.selectedEmailId || !state.selectedEmail || !action) return;
const originalHtml = button?.innerHTML || '';
try {
if (button) {
button.disabled = true;
button.innerHTML = '< span class = "spinner-border spinner-border-sm me-1" > < / span > Udfører…';
}
const title = String(document.getElementById('v2CaseTitle')?.value
|| state.selectedEmail.subject
|| '').trim();
const result = await apiFetch(`/api/v1/emails/${state.selectedEmailId}/quick-action`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, titel: title }),
});
if (action === 'archive_entity_mail') {
state.selectedEmailId = null;
state.selectedEmail = null;
renderDetail(null);
await loadEmails();
} else {
await selectEmail(state.selectedEmailId);
}
setDetailStatus(result?.message || 'Handlingen er udført');
} catch (error) {
setDetailStatus(`Handlingen fejlede: ${error.message}`);
window.alert(`Handlingen kunne ikke udføres:\n${error.message}`);
} finally {
if (button & & document.body.contains(button)) {
button.disabled = false;
button.innerHTML = originalHtml;
}
2026-05-12 08:41:13 +02:00
}
}
async function linkSelectedSag(sagId) {
if (!state.selectedEmailId || !sagId) return;
try {
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/link-sag`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sag_id: Number(sagId), relation_type: 'mail', mark_processed: true }),
});
setDetailStatus(`Email linket til SAG #${sagId}`);
await selectEmail(state.selectedEmailId);
} catch (error) {
setDetailStatus(`Kunne ikke linke sag: ${error.message}`);
}
}
function setSupplierStatus(message, type) {
const el = document.getElementById('v2SupplierStatus');
if (!el) return;
el.className = `emails-v2-inline-status ${type || ''}`.trim();
el.textContent = message || '';
}
function renderVendorSuggestion() {
const host = document.getElementById('v2VendorSuggestion');
if (!host) return;
const suggestion = state.vendorSuggestion;
if (!suggestion) {
host.innerHTML = '< div class = "small text-muted" > Ingen forslag endnu< / div > ';
return;
}
host.innerHTML = `
< div class = "emails-v2-kv" > < div class = "k" > Navn< / div > < div class = "v" > ${escapeHtml(suggestion.name || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > CVR< / div > < div class = "v" > ${escapeHtml(suggestion.cvr_number || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Adresse< / div > < div class = "v" > ${escapeHtml(suggestion.address || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Telefon< / div > < div class = "v" > ${escapeHtml(suggestion.phone || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Email< / div > < div class = "v" > ${escapeHtml(suggestion.email || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Domæne< / div > < div class = "v" > ${escapeHtml(suggestion.domain || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Match< / div > < div class = "v" > Vendor ID: ${escapeHtml(String(suggestion.vendor_id || '-'))} • Score: ${escapeHtml(String(suggestion.match_score || 0))}< / div > < / div >
`;
}
async function extractVendorSuggestionCurrent() {
if (!state.selectedEmailId) return;
try {
setSupplierStatus('Udtrækker leverandørforslag...');
const suggestion = await apiFetch(`/api/v1/emails/${state.selectedEmailId}/extract-vendor-suggestion`, {
method: 'POST',
});
state.vendorSuggestion = suggestion || null;
renderVendorSuggestion();
2026-07-30 20:11:43 +02:00
renderClassificationDecision();
2026-05-12 08:41:13 +02:00
setSupplierStatus('Forslag opdateret', 'success');
} catch (error) {
2026-07-30 20:11:43 +02:00
state.vendorSuggestion = {};
renderClassificationDecision();
2026-05-12 08:41:13 +02:00
setSupplierStatus(`Kunne ikke udtrække forslag: ${error.message}`, 'error');
}
}
function setDomainStatus(message, type) {
const el = document.getElementById('v2DomainStatus');
if (!el) return;
el.className = `emails-v2-inline-status ${type || ''}`.trim();
el.textContent = message || '';
}
function renderDomainCustomerSuggestion() {
const host = document.getElementById('v2DomainSuggestion');
if (!host) return;
const data = state.domainCustomerSuggestion;
if (!data) {
host.innerHTML = '< div class = "small text-muted" > Ingen domæneforslag endnu< / div > ';
return;
}
if (data.has_customer) {
host.innerHTML = '< div class = "small text-success" > Email har allerede kunde-link.< / div > ';
return;
}
if (data.ignored) {
host.innerHTML = `< div class = "small text-muted" > Forslag ignoreret (${escapeHtml(data.reason || 'ukendt')})< / div > `;
return;
}
if (!data.suggestion) {
host.innerHTML = '< div class = "small text-muted" > Ingen match på domæne< / div > ';
return;
}
host.innerHTML = `
< div class = "emails-v2-kv" > < div class = "k" > Domæne< / div > < div class = "v" > ${escapeHtml(data.domain || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Kunde< / div > < div class = "v" > ${escapeHtml(data.suggestion.customer_name || '-')} (#${escapeHtml(String(data.suggestion.customer_id || '-'))})< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Sikkerhed< / div > < div class = "v" > ${escapeHtml(data.suggestion.confidence || '-')} • score ${escapeHtml(String(data.suggestion.score || 0))}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Kilde< / div > < div class = "v" > ${escapeHtml(data.suggestion.source || '-')}< / div > < / div >
< button id = "v2ApplyDomainSuggestion" class = "btn btn-sm btn-outline-primary mt-2" > Anvend kunde-link< / button >
`;
document.getElementById('v2ApplyDomainSuggestion')?.addEventListener('click', applyDomainCustomerSuggestion);
}
async function loadDomainCustomerSuggestionCurrent() {
if (!state.selectedEmailId) return;
try {
setDomainStatus('Henter domæneforslag...');
const suggestion = await apiFetch(`/api/v1/emails/${state.selectedEmailId}/domain-customer-suggestion`);
state.domainCustomerSuggestion = suggestion || null;
renderDomainCustomerSuggestion();
2026-07-30 20:11:43 +02:00
renderClassificationDecision();
2026-05-12 08:41:13 +02:00
setDomainStatus('Forslag opdateret', 'success');
} catch (error) {
2026-07-30 20:11:43 +02:00
state.domainCustomerSuggestion = {
domain: String(state.selectedEmail?.sender_email || '').split('@')[1] || null,
suggestion: null,
};
renderClassificationDecision();
2026-05-12 08:41:13 +02:00
setDomainStatus(`Kunne ikke hente forslag: ${error.message}`, 'error');
}
}
async function applyDomainCustomerSuggestion() {
if (!state.selectedEmailId) return;
const suggestion = state.domainCustomerSuggestion?.suggestion;
if (!suggestion?.customer_id) return;
try {
setDomainStatus('Anvender kunde-link...');
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/link`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customer_id: Number(suggestion.customer_id) }),
});
await selectEmail(state.selectedEmailId);
setDomainStatus('Kunde-link anvendt', 'success');
} catch (error) {
setDomainStatus(`Kunne ikke anvende forslag: ${error.message}`, 'error');
}
}
2026-07-30 20:11:43 +02:00
async function createCustomerFromSuggestion() {
if (!state.selectedEmailId || !state.selectedEmail) return;
const email = state.selectedEmail;
const domain = state.domainCustomerSuggestion?.domain
|| String(email.sender_email || '').split('@')[1]
|| null;
const name = String(email.sender_name || domain || '').trim();
if (!name) {
setDetailStatus('Kunden kan ikke oprettes uden et navn');
return;
}
try {
setDetailStatus(`Opretter kunden ${name}…`);
const customer = await apiFetch('/api/v1/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
email: email.sender_email || null,
email_domain: domain,
is_active: true,
country: 'DK',
}),
});
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/link`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customer_id: Number(customer.id) }),
});
setDetailStatus(`Kunden ${name} blev oprettet og tilknyttet`);
await selectEmail(state.selectedEmailId);
} catch (error) {
setDetailStatus(`Kunne ikke oprette kunden: ${error.message}`);
}
}
async function linkVendorSuggestion() {
const vendorId = Number(state.vendorSuggestion?.vendor_id || 0);
if (!state.selectedEmailId || !vendorId) return;
try {
setDetailStatus('Tilknytter leverandøren…');
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/link`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplier_id: vendorId }),
});
setDetailStatus('Leverandøren blev tilknyttet');
await selectEmail(state.selectedEmailId);
} catch (error) {
setDetailStatus(`Kunne ikke tilknytte leverandøren: ${error.message}`);
}
}
async function createVendorFromSuggestion() {
if (!state.selectedEmailId || !state.selectedEmail) return;
const email = state.selectedEmail;
const suggestion = state.vendorSuggestion || {};
const senderDomain = String(email.sender_email || '').split('@')[1] || null;
const name = String(suggestion.name || email.sender_name || senderDomain || '').trim();
if (!name) {
setDetailStatus('Leverandøren kan ikke oprettes uden et navn');
return;
}
try {
setDetailStatus(`Opretter leverandøren ${name}…`);
const vendor = await apiFetch('/api/v1/vendors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
cvr_number: suggestion.cvr_number || null,
email: suggestion.email || email.sender_email || null,
phone: suggestion.phone || null,
address: suggestion.address || null,
domain: suggestion.domain || senderDomain,
category: 'supplier',
notes: `Oprettet fra email #${state.selectedEmailId}`,
is_active: true,
}),
});
await apiFetch(`/api/v1/emails/${state.selectedEmailId}/link`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplier_id: Number(vendor.id) }),
});
setDetailStatus(`Leverandøren ${name} blev oprettet og tilknyttet`);
await selectEmail(state.selectedEmailId);
} catch (error) {
setDetailStatus(`Kunne ikke oprette leverandøren: ${error.message}`);
}
}
2026-05-12 08:41:13 +02:00
async function searchSager(query) {
const host = document.getElementById('v2SagResults');
if (!host) return;
const q = String(query || '').trim();
if (q.length < 2 ) {
host.innerHTML = '';
return;
}
host.innerHTML = '< div class = "p-2 small text-muted" > Søger...< / div > ';
try {
const rows = await apiFetch(`/api/v1/emails/search-sager?q=${encodeURIComponent(q)}&limit=15`);
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
host.innerHTML = '< div class = "p-2 small text-muted" > Ingen sager fundet< / div > ';
return;
}
host.innerHTML = list.map((item) => `
< div class = "emails-v2-sag-result" data-sag-id = "${Number(item.id)}" >
< div class = "fw-semibold" > #${Number(item.id)} ${escapeHtml(item.titel || 'Uden titel')}< / div >
< div class = "small text-muted" > ${escapeHtml(item.customer_name || 'Ukendt kunde')} • ${escapeHtml(item.status || '-')}< / div >
< / div >
`).join('');
host.querySelectorAll('[data-sag-id]').forEach((node) => {
node.addEventListener('click', () => {
linkSelectedSag(Number(node.getAttribute('data-sag-id')));
});
});
} catch (error) {
host.innerHTML = `< div class = "p-2 small text-danger" > Fejl: ${escapeHtml(error.message)}< / div > `;
}
}
function renderDetail(email) {
const mailHeader = document.getElementById('v2MailHeader');
2026-07-30 20:11:43 +02:00
const quickActions = document.getElementById('v2QuickActions');
2026-05-12 08:41:13 +02:00
const mailBody = document.getElementById('v2MailBody');
const sideActions = document.getElementById('v2SideActions');
2026-07-30 20:11:43 +02:00
if (!mailHeader || !quickActions || !mailBody || !sideActions) return;
2026-05-12 08:41:13 +02:00
if (!email) {
mailHeader.innerHTML = 'Vælg en email for at se info';
2026-07-30 20:11:43 +02:00
quickActions.innerHTML = '';
2026-05-12 08:41:13 +02:00
mailBody.className = 'emails-v2-detail-empty';
mailBody.textContent = 'Vælg en email fra listen';
sideActions.className = 'emails-v2-detail-empty';
sideActions.textContent = 'Vælg en email for handlinger';
setMailStatus('Ingen email valgt');
return;
}
const headerLink = email.linked_case_id
? `< a href = "/sag/${Number(email.linked_case_id)}/v3" > SAG #${Number(email.linked_case_id)}< / a > `
: 'Ikke linket til sag';
mailHeader.innerHTML = `
< div class = "emails-v2-subject" > ${escapeHtml(email.subject || '(Ingen emne)')}< / div >
< div class = "emails-v2-topmeta mb-0" >
< span > Fra: ${escapeHtml(email.sender_name || email.sender_email || '-')}< / span >
< span > Modtaget: ${escapeHtml(formatDate(email.received_date))}< / span >
< span > Status: ${escapeHtml(email.status || '-')}< / span >
< span > ${headerLink}< / span >
< / div >
`;
2026-07-30 20:11:43 +02:00
const hasCustomer = Boolean(email.customer_id);
const hasSupplier = Boolean(email.supplier_id);
const hasIdentity = hasCustomer || hasSupplier;
2026-09-11 13:51:10 +02:00
const supplierIdentity = `${email.supplier_name || ''} ${email.sender_email || ''}`.toLowerCase();
const isGlobalConnect = supplierIdentity.includes('globalconnect');
const classification = String(email.classification || '').toLowerCase();
const recommendedFlow = classification === 'invoice'
? 'create_supplier_invoice'
: (classification === 'customer_email' ? 'create_customer_case' : '');
const recommendationBadge = (flow) => recommendedFlow === flow
? '< span class = "flow-badge" > ANBEFALET< / span > '
: '';
const recommendationClass = (flow) => recommendedFlow === flow ? ' recommended' : '';
2026-07-30 20:11:43 +02:00
quickActions.className = 'emails-v2-quick-toolbar';
quickActions.innerHTML = email.linked_case_id ? `
< a class = "btn btn-sm btn-primary w-100" href = "/sag/${Number(email.linked_case_id)}/v3" >
< i class = "bi bi-box-arrow-up-right me-1" > < / i > Åbn SAG #${Number(email.linked_case_id)}
< / a >
` : `
2026-09-11 13:51:10 +02:00
< div class = "emails-v2-flow-picker" >
< div class = "fw-semibold mb-2" > Vælg hvordan mailen skal behandles< / div >
< div class = "emails-v2-flow-grid" aria-label = "Vælg mailflow" >
< button class = "emails-v2-flow-choice flow-support${recommendationClass('create_customer_case')}" data-v2-quick-action = "create_customer_case" $ { hasCustomer ? ' ' : ' disabled ' } title = "${hasCustomer ? 'Opretter en almindelig kundesag' : 'Fastslå kunden først'}" >
${recommendationBadge('create_customer_case')}
< span class = "flow-title" > < i class = "bi bi-headset me-1" > < / i > Support< / span >
< span class = "flow-description" > Opret en almindelig kundesag< / span >
2026-07-30 20:11:43 +02:00
< / button >
2026-09-11 13:51:10 +02:00
< button class = "emails-v2-flow-choice flow-invoice${recommendationClass('create_supplier_invoice')}" data-v2-quick-action = "create_supplier_invoice" $ { hasSupplier ? ' ' : ' disabled ' } title = "${hasSupplier ? 'Sender fakturabilaget til behandling' : 'Fastslå leverandøren først'}" >
${recommendationBadge('create_supplier_invoice')}
< span class = "flow-title" > < i class = "bi bi-receipt me-1" > < / i > Leverandørfaktura< / span >
< span class = "flow-description" > ${isGlobalConnect ? 'Internetfaktura genkendt · opdaterer Internetforbindelser' : 'Aflæs PDF og kør leverandørflowet'}< / span >
2026-07-30 20:11:43 +02:00
< / button >
2026-09-11 13:51:10 +02:00
< button class = "emails-v2-flow-choice flow-accounting${recommendationClass('create_accounting_case')}" data-v2-quick-action = "create_accounting_case" $ { hasCustomer ? ' ' : ' disabled ' } title = "${hasCustomer ? 'Opretter sag og tildeler Bogholderi' : 'Fastslå kunden først'}" >
${recommendationBadge('create_accounting_case')}
< span class = "flow-title" > < i class = "bi bi-calculator me-1" > < / i > Bogholderi< / span >
< span class = "flow-description" > Opret kundesag hos Bogholderi< / span >
2026-07-30 20:11:43 +02:00
< / button >
2026-09-11 13:51:10 +02:00
< button class = "emails-v2-flow-choice flow-archive${recommendationClass('archive_entity_mail')}" data-v2-quick-action = "archive_entity_mail" $ { hasIdentity ? ' ' : ' disabled ' } title = "${hasIdentity ? 'Arkiverer kun mailen; opretter intet' : 'Fastslå kunde eller leverandør først'}" >
${recommendationBadge('archive_entity_mail')}
< span class = "flow-title" > < i class = "bi bi-archive me-1" > < / i > Kun arkivér< / span >
< span class = "flow-description" > Ingen sag eller anden behandling< / span >
2026-07-30 20:11:43 +02:00
< / button >
2026-09-11 13:51:10 +02:00
< / div >
2026-07-30 20:11:43 +02:00
< / div >
`;
2026-05-12 08:41:13 +02:00
mailBody.className = 'emails-v2-mail-body';
const hasAttachments = Array.isArray(email.attachments) & & email.attachments.length > 0;
const rawHtml = String(email.body_html || '').trim();
const sanitizedHtml = sanitizeEmailHtml(rawHtml);
const renderedMessage = sanitizedHtml || formatEmailPlainText(email.body_text || 'Ingen indhold');
const messageClass = sanitizedHtml ? 'emails-v2-body html' : 'emails-v2-body';
mailBody.innerHTML = `
< div class = "emails-v2-card" >
< h6 > Besked< / h6 >
< div class = "${messageClass}" > ${renderedMessage}< / div >
< / div >
${hasAttachments ? `
< div class = "emails-v2-card" >
< h6 > Vedhæftninger (${email.attachments.length})< / h6 >
< div class = "emails-v2-attachments" >
${email.attachments.map((att) => {
const name = att.filename || `Vedhæftning ${att.id}`;
return `< a class = "btn btn-sm btn-outline-secondary" href = "/api/v1/emails/${Number(email.id)}/attachments/${Number(att.id)}" > ${escapeHtml(name)}< / a > `;
}).join('')}
< / div >
< / div > ` : ''}
`;
sideActions.className = 'emails-v2-actions-pane';
sideActions.innerHTML = `
2026-07-30 20:11:43 +02:00
< div id = "v2DecisionCard" class = "email-ai-decision" >
< div class = "d-flex align-items-center gap-2 text-muted" >
< span class = "spinner-border spinner-border-sm" > < / span >
Systemet vurderer mailtype og handlinger…
< / div >
< / div >
2026-09-11 13:51:10 +02:00
< details class = "emails-v2-card" >
< summary class = "fw-semibold" style = "cursor:pointer;" >
< i class = "bi bi-three-dots me-1" > < / i > Flere handlinger
< / summary >
< div class = "mt-3" >
2026-07-28 14:18:24 +02:00
${email.linked_case_id ? `
2026-09-11 13:51:10 +02:00
< div class = "emails-primary-action rounded p-3 mb-3" >
2026-07-28 14:18:24 +02:00
< h6 > Allerede knyttet til sag< / h6 >
< a class = "btn btn-primary w-100" href = "/sag/${Number(email.linked_case_id)}/v3" >
< i class = "bi bi-box-arrow-up-right me-2" > < / i > Åbn SAG #${Number(email.linked_case_id)}
< / a >
< button id = "v2Processed" class = "btn btn-sm btn-outline-success w-100 mt-2" >
< i class = "bi bi-check2 me-1" > < / i > Markér mailen behandlet
< / button >
< / div > ` : `
2026-09-11 13:51:10 +02:00
< details class = "border rounded p-3 mb-3" >
< summary class = "fw-semibold" style = "cursor:pointer;" > Opret en anden type sag< / summary >
< div class = "mt-3" >
2026-07-28 14:18:24 +02:00
< input id = "v2CaseTitle" class = "form-control form-control-sm mb-2" value = "${escapeHtml(email.subject || '')}" placeholder = "Sagens titel" >
< div class = "d-flex gap-2" >
< select id = "v2CaseType" class = "form-select form-select-sm" >
2026-07-30 20:11:43 +02:00
< option value = "support" $ { ! email . supplier_id ? ' selected ' : ' ' } > Support< / option >
2026-07-28 14:18:24 +02:00
< option value = "bogholderi" > Bogholderi< / option >
2026-07-30 20:11:43 +02:00
< option value = "leverandor" $ { email . supplier_id ? ' selected ' : ' ' } > Leverandør< / option >
2026-07-28 14:18:24 +02:00
< option value = "helhedsopgave" > Projekt/helhedsopgave< / option >
< option value = "andet" > Andet< / option >
< / select >
< button id = "v2CreateCase" class = "btn btn-primary text-nowrap" >
< i class = "bi bi-plus-lg me-1" > < / i > Opret
< / button >
2026-05-12 08:41:13 +02:00
< / div >
2026-09-11 13:51:10 +02:00
< / div >
< / details > `}
2026-05-12 08:41:13 +02:00
2026-09-11 13:51:10 +02:00
< div class = "border rounded p-3 mb-3" >
2026-07-28 14:18:24 +02:00
< h6 > ${email.linked_case_id ? 'Skift sagstilknytning' : 'Eller knyt til eksisterende sag'}< / h6 >
< input id = "v2SagSearch" class = "form-control form-control-sm mb-2" placeholder = "Skriv sagsnummer eller titel…" >
2026-05-12 08:41:13 +02:00
< div id = "v2SagResults" class = "emails-v2-sag-results" > < / div >
< / div >
2026-09-11 13:51:10 +02:00
< div class = "border rounded p-3 mb-3" >
2026-07-28 14:18:24 +02:00
< h6 > Hurtige mailhandlinger< / h6 >
< div class = "emails-v2-actions mb-0" >
< button id = "v2ReadToggle" class = "btn btn-sm btn-outline-secondary" >
< i class = "bi bi-envelope${email.is_read ? '' : '-open'} me-1" > < / i > ${email.is_read ? 'Ulæst' : 'Læst'}
< / button >
< button id = "v2Archive" class = "btn btn-sm btn-outline-secondary" >
< i class = "bi bi-archive me-1" > < / i > Arkivér
< / button >
${email.linked_case_id ? '' : `
< button id = "v2Processed" class = "btn btn-sm btn-outline-success" >
< i class = "bi bi-check2 me-1" > < / i > Behandlet
< / button > `}
2026-05-12 08:41:13 +02:00
< / div >
< / div >
2026-09-11 13:51:10 +02:00
< details class = "border rounded p-3 mb-3" >
2026-07-28 14:18:24 +02:00
< summary class = "small fw-semibold" style = "cursor:pointer;" > Avanceret behandling< / summary >
< div class = "emails-v2-actions mt-3 mb-2" >
< button id = "v2Reprocess" class = "btn btn-sm btn-outline-warning" > Genbehandl< / button >
< button id = "v2ExecuteWorkflows" class = "btn btn-sm btn-outline-dark" > Kør workflows< / button >
< button id = "v2AutoRun" class = "btn btn-sm btn-outline-danger" disabled > Autokør< / button >
< button id = "v2AutoParse" class = "btn btn-sm btn-outline-primary" > Find tråd/sag automatisk< / button >
< / div >
< div id = "v2WorkflowPreview" > < div class = "small text-muted" > Henter workflow-preview…< / div > < / div >
< / details >
2026-09-11 13:51:10 +02:00
< details class = "border rounded p-3 mb-3" >
2026-07-28 14:18:24 +02:00
< summary class = "small fw-semibold" style = "cursor:pointer;" > Leverandør og kundematch< / summary >
< h6 class = "mt-3" > Leverandør faktura< / h6 >
2026-05-12 08:41:13 +02:00
< div class = "emails-v2-kv" > < div class = "k" > Leverandør< / div > < div class = "v" > ${escapeHtml(email.extracted_vendor_name || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > CVR< / div > < div class = "v" > ${escapeHtml(email.extracted_vendor_cvr || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Faktura nr< / div > < div class = "v" > ${escapeHtml(email.extracted_invoice_number || '-')}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Beløb< / div > < div class = "v" > ${escapeHtml(String(email.extracted_amount || '-'))}< / div > < / div >
< div class = "emails-v2-kv" > < div class = "k" > Forfald< / div > < div class = "v" > ${escapeHtml(email.extracted_due_date || '-')}< / div > < / div >
< div class = "emails-v2-right-actions mt-2" >
< button id = "v2ExtractVendor" class = "btn btn-sm btn-outline-warning" > Udtræk leverandørforslag< / button >
< / div >
< div id = "v2VendorSuggestion" class = "mt-2" > < div class = "small text-muted" > Ingen forslag endnu< / div > < / div >
< div id = "v2SupplierStatus" class = "emails-v2-inline-status" > < / div >
2026-07-28 14:18:24 +02:00
< hr >
< h6 > Automatisk kundematch< / h6 >
2026-05-12 08:41:13 +02:00
< div class = "emails-v2-right-actions" >
< button id = "v2DomainSuggestionBtn" class = "btn btn-sm btn-outline-secondary" > Hent domæne-kundeforslag< / button >
< / div >
< div id = "v2DomainSuggestion" class = "mt-2" > < div class = "small text-muted" > Ingen domæneforslag endnu< / div > < / div >
< div id = "v2DomainStatus" class = "emails-v2-inline-status" > < / div >
2026-07-28 14:18:24 +02:00
< / details >
2026-05-12 08:41:13 +02:00
2026-09-11 13:51:10 +02:00
< details class = "border rounded p-3" >
2026-05-12 08:41:13 +02:00
< summary class = "small fw-semibold" style = "cursor:pointer;" > Avanceret metadata< / summary >
< pre class = "small mb-0 mt-2" style = "white-space: pre-wrap;" > ${escapeHtml(JSON.stringify({
email_id: email.id,
message_id: email.message_id,
in_reply_to: email.in_reply_to,
email_references: email.email_references,
classification: email.classification,
confidence_score: email.confidence_score,
linked_case_id: email.linked_case_id,
}, null, 2))}< / pre >
< / details >
2026-09-11 13:51:10 +02:00
< / div >
< / details >
2026-05-12 08:41:13 +02:00
`;
document.getElementById('v2ReadToggle')?.addEventListener('click', () => patchReadState(!Boolean(email.is_read)));
document.getElementById('v2Archive')?.addEventListener('click', archiveCurrent);
document.getElementById('v2Processed')?.addEventListener('click', markProcessedCurrent);
document.getElementById('v2Reprocess')?.addEventListener('click', reprocessCurrent);
document.getElementById('v2ExecuteWorkflows')?.addEventListener('click', executeWorkflowsCurrent);
document.getElementById('v2AutoRun')?.addEventListener('click', autoRunWorkflowsCurrent);
document.getElementById('v2AutoParse')?.addEventListener('click', autoParseAndLinkCurrent);
document.getElementById('v2CreateCase')?.addEventListener('click', createCaseFromCurrent);
document.getElementById('v2ExtractVendor')?.addEventListener('click', extractVendorSuggestionCurrent);
document.getElementById('v2DomainSuggestionBtn')?.addEventListener('click', loadDomainCustomerSuggestionCurrent);
2026-07-30 20:11:43 +02:00
document.querySelectorAll('[data-v2-quick-action]').forEach((button) => {
button.addEventListener('click', () => {
runQuickAction(button.getAttribute('data-v2-quick-action'), button);
});
});
2026-05-12 08:41:13 +02:00
state.vendorSuggestion = null;
state.domainCustomerSuggestion = null;
state.workflowPreview = null;
renderVendorSuggestion();
renderDomainCustomerSuggestion();
renderWorkflowPreview();
loadWorkflowPreviewCurrent();
2026-07-30 20:11:43 +02:00
const vendorClassifications = new Set(['invoice', 'order_confirmation', 'freight_note']);
if (email.supplier_id || vendorClassifications.has(String(email.classification || '').toLowerCase())
|| email.extracted_vendor_name || email.extracted_vendor_cvr) {
extractVendorSuggestionCurrent();
} else {
state.vendorSuggestion = {};
renderClassificationDecision();
}
2026-05-12 08:41:13 +02:00
setMailStatus(`Email #${email.id} vises`);
document.getElementById('v2SagSearch')?.addEventListener('input', (event) => {
const query = event.target.value;
clearTimeout(state.sagSearchTimer);
state.sagSearchTimer = setTimeout(() => searchSager(query), 220);
});
}
2026-07-28 14:18:24 +02:00
async function uploadEmailFiles(files) {
const accepted = Array.from(files || []).filter((file) =>
/\.(eml|msg)$/i.test(file.name || '')
);
if (!accepted.length) {
setListStatus('Vælg .eml- eller .msg-filer');
return;
}
const zone = document.getElementById('v2UploadZone');
const label = document.getElementById('v2UploadLabel');
const original = label?.innerHTML;
if (zone) {
zone.classList.add('busy');
}
if (label) {
label.innerHTML = `< span class = "spinner-border spinner-border-sm me-1" > < / span > Importerer ${accepted.length} mail${accepted.length === 1 ? '' : 's'}…`;
}
try {
const formData = new FormData();
accepted.forEach((file) => formData.append('files', file));
const result = await apiFetch('/api/v1/emails/upload', {
method: 'POST',
body: formData,
});
const uploaded = Number(result?.uploaded || 0);
const duplicates = Number(result?.duplicates || 0);
setListStatus(`${uploaded} importeret${duplicates ? ` · ${duplicates} fandtes allerede` : ''}`);
state.filter = 'active';
state.selectedEmailId = null;
await loadEmails();
} catch (error) {
setListStatus(`Import fejlede: ${error.message}`);
} finally {
if (zone) {
zone.classList.remove('busy', 'dragover');
}
if (label) label.innerHTML = original;
}
}
function setupUploadZone() {
const zone = document.getElementById('v2UploadZone');
const input = document.getElementById('v2UploadInput');
if (!zone || !input || zone.dataset.ready === 'true') return;
zone.dataset.ready = 'true';
zone.addEventListener('click', () => input.click());
zone.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
input.click();
}
});
['dragenter', 'dragover'].forEach((type) => zone.addEventListener(type, (event) => {
event.preventDefault();
zone.classList.add('dragover');
}));
['dragleave', 'drop'].forEach((type) => zone.addEventListener(type, (event) => {
event.preventDefault();
zone.classList.remove('dragover');
}));
zone.addEventListener('drop', (event) => uploadEmailFiles(event.dataTransfer?.files));
input.addEventListener('change', () => {
uploadEmailFiles(input.files);
input.value = '';
});
}
function selectAdjacentEmail(offset) {
if (!state.emails.length) return;
const current = state.emails.findIndex((email) => Number(email.id) === Number(state.selectedEmailId));
const next = Math.min(Math.max((current < 0 ? 0 : current ) + offset , 0 ) , state . emails . length - 1 ) ;
selectEmail(Number(state.emails[next].id));
}
function setupKeyboardShortcuts() {
document.addEventListener('keydown', (event) => {
const target = event.target;
const typing = target instanceof HTMLInputElement
|| target instanceof HTMLTextAreaElement
|| target instanceof HTMLSelectElement
|| target?.isContentEditable;
if (event.key === '/' & & !typing) {
event.preventDefault();
document.getElementById('v2Search')?.focus();
} else if (!typing & & event.key.toLowerCase() === 'j') {
selectAdjacentEmail(1);
} else if (!typing & & event.key.toLowerCase() === 'k') {
selectAdjacentEmail(-1);
} else if (!typing & & event.key.toLowerCase() === 'r') {
loadEmails();
}
});
}
2026-05-12 08:41:13 +02:00
function setupEvents() {
document.getElementById('v2Search')?.addEventListener('input', (event) => {
const value = String(event.target.value || '').trim();
clearTimeout(state.searchTimer);
state.searchTimer = setTimeout(() => {
state.query = value;
loadEmails();
}, 250);
});
document.getElementById('v2Refresh')?.addEventListener('click', () => loadEmails());
2026-07-28 14:18:24 +02:00
setupUploadZone();
setupKeyboardShortcuts();
2026-05-12 08:41:13 +02:00
}
document.addEventListener('DOMContentLoaded', async () => {
setupEvents();
renderFilters();
renderDetail(null);
await loadEmails();
});
})();
< / script >
{% endblock %}