2025-12-11 12:45:29 +01:00
{% extends "shared/frontend/base.html" %}
{% block title %}Email - BMC Hub{% endblock %}
{% block extra_css %}
< style >
/* Email Layout - 3 Column Grid */
.email-container {
display: flex;
gap: 1rem;
height: calc(100vh - 140px);
overflow: hidden;
}
/* Left Sidebar - Email List (25%) */
.email-list-sidebar {
flex: 0 0 400px;
display: flex;
flex-direction: column;
background: var(--bg-card);
border-radius: var(--border-radius);
overflow: hidden;
}
.email-list-header {
padding: 1rem;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.email-list-filters {
padding: 0.5rem 1rem;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.filter-pill {
background: var(--bg-body);
border: 1px solid rgba(0,0,0,0.1);
color: var(--text-secondary);
padding: 0.4rem 0.9rem;
border-radius: 20px;
font-size: 0.85rem;
transition: all 0.2s;
cursor: pointer;
white-space: nowrap;
}
.filter-pill:hover, .filter-pill.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.filter-pill .count {
opacity: 0.7;
margin-left: 0.3rem;
}
.email-list-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.email-item {
padding: 1rem;
border-bottom: 1px solid rgba(0,0,0,0.05);
cursor: pointer;
transition: all 0.2s;
display: flex;
gap: 0.75rem;
}
.email-item:hover:not(.active) {
background: rgba(15, 76, 117, 0.08);
}
.email-item.unread:not(.active) {
background: rgba(15, 76, 117, 0.05);
border-left: 3px solid var(--accent);
}
.email-item.unread .email-subject {
font-weight: 700;
}
.email-item.active {
background: var(--accent) !important;
border-left: 4px solid #0a3a5c !important;
}
.email-item.active .email-subject,
.email-item.active .email-sender,
.email-item.active .email-preview,
.email-item.active .email-time {
color: white !important;
}
.email-item.active .badge {
background: rgba(255, 255, 255, 0.2) !important;
color: white !important;
border-color: rgba(255, 255, 255, 0.3) !important;
}
.email-item.active .sender-avatar {
border: 2px solid rgba(255, 255, 255, 0.3);
}
.sender-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--accent-light);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.85rem;
flex-shrink: 0;
}
.email-item.active .sender-avatar {
background: rgba(255,255,255,0.2);
color: white;
}
.email-item-content {
flex: 1;
min-width: 0;
}
.email-item-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.3rem;
}
.email-sender {
font-weight: 600;
font-size: 0.9rem;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.email-time {
font-size: 0.75rem;
color: var(--text-secondary);
white-space: nowrap;
margin-left: 0.5rem;
}
.email-subject {
font-size: 0.9rem;
color: var(--text-primary);
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.email-preview {
font-size: 0.8rem;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 0.3rem;
}
.email-meta {
display: flex;
gap: 0.5rem;
align-items: center;
}
.classification-badge {
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
border-radius: 10px;
font-weight: 500;
}
.classification-invoice { background: #d4edda; color: #155724; }
.classification-order_confirmation { background: #d1ecf1; color: #0c5460; }
.classification-freight_note { background: #fff3cd; color: #856404; }
.classification-time_confirmation { background: #e2e3e5; color: #383d41; }
.classification-case_notification { background: #cce5ff; color: #004085; }
.classification-bankruptcy { background: #f8d7da; color: #721c24; }
.classification-spam { background: #343a40; color: #fff; }
.classification-general { background: #e9ecef; color: #495057; }
[data-bs-theme="dark"] .classification-badge {
opacity: 0.9;
}
.unread-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
margin-right: 0.3rem;
}
/* Center Pane - Email Content (50%) */
.email-content-pane {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg-card);
border-radius: var(--border-radius);
overflow: hidden;
}
.email-content-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.email-content-subject {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 1rem;
}
.email-content-meta {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.sender-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.sender-details {
display: flex;
flex-direction: column;
}
.sender-name {
font-weight: 600;
font-size: 0.95rem;
}
.sender-email {
font-size: 0.8rem;
color: var(--text-secondary);
}
.email-timestamp {
color: var(--text-secondary);
font-size: 0.85rem;
}
.email-actions {
padding: 1rem 1.5rem;
border-bottom: 1px solid rgba(0,0,0,0.05);
background: var(--bg-body);
2025-12-11 12:57:14 +01:00
flex-wrap: wrap;
2025-12-11 12:45:29 +01:00
}
.email-actions .btn-primary {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.email-actions .btn-primary:hover {
background: #0a3a5c;
border-color: #0a3a5c;
}
.email-body {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
line-height: 1.6;
}
.email-body iframe {
width: 100%;
border: none;
min-height: 400px;
}
.email-attachments {
padding: 1rem 1.5rem;
border-top: 1px solid rgba(0,0,0,0.1);
background: var(--bg-body);
}
.attachment-item {
padding: 0.75rem 1rem;
background: var(--bg-card);
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
margin-bottom: 0.5rem;
transition: all 0.2s;
color: var(--text-primary);
}
.attachment-item:hover {
background: var(--bg-hover);
border-color: var(--accent);
}
.email-attachments {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(0,0,0,0.1);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
}
.empty-state i {
font-size: 4rem;
opacity: 0.3;
margin-bottom: 1rem;
}
/* Right Sidebar - AI Analysis (25%) */
.email-analysis-sidebar {
flex: 0 0 380px;
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
}
.analysis-card {
background: var(--bg-card);
border-radius: var(--border-radius);
padding: 1.25rem;
}
.analysis-card h6 {
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.confidence-meter {
margin-bottom: 1rem;
}
.confidence-bar {
height: 8px;
background: rgba(0,0,0,0.1);
border-radius: 4px;
overflow: hidden;
margin-top: 0.5rem;
}
.confidence-fill {
height: 100%;
background: linear-gradient(90deg, #dc3545, #ffc107, #28a745);
transition: width 0.3s;
}
.confidence-label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 0.3rem;
}
.classification-select {
width: 100%;
padding: 0.6rem;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
background: var(--bg-body);
color: var(--text-primary);
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.metadata-list {
list-style: none;
padding: 0;
margin: 0;
}
.metadata-item {
padding: 0.5rem 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
font-size: 0.85rem;
}
.metadata-item:last-child {
border-bottom: none;
}
.metadata-label {
color: var(--text-secondary);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.2rem;
}
.metadata-value {
color: var(--text-primary);
font-weight: 500;
}
.rules-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--accent-light);
border-radius: 8px;
font-size: 0.85rem;
color: var(--accent);
margin-bottom: 0.75rem;
}
/* Responsive Design */
@media (max-width: 1200px) {
.email-list-sidebar {
flex: 0 0 280px;
}
.email-analysis-sidebar {
flex: 0 0 260px;
}
}
@media (max-width: 992px) {
.email-container {
flex-direction: column;
height: auto;
}
.email-list-sidebar,
.email-content-pane,
.email-analysis-sidebar {
flex: 0 0 auto;
max-height: 600px;
}
}
@media (max-width: 768px) {
.email-container {
gap: 0.5rem;
}
.email-list-sidebar {
max-height: 400px;
}
.email-analysis-sidebar {
display: none;
}
.filter-pill {
font-size: 0.75rem;
padding: 0.3rem 0.7rem;
}
}
/* Keyboard Navigation Hint */
.keyboard-hint {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--bg-card);
padding: 0.5rem 1rem;
border-radius: 20px;
box-shadow: 0 4px 15px rgba(0,0,0,0.15);
font-size: 0.8rem;
color: var(--text-secondary);
opacity: 0.7;
transition: opacity 0.2s;
}
.keyboard-hint:hover {
opacity: 1;
}
/* Loading States */
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
padding: 3rem;
}
/* Bulk Actions Toolbar */
.bulk-actions-toolbar {
display: none;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
background: var(--accent);
color: white;
border-radius: var(--border-radius);
margin-bottom: 1rem;
}
.bulk-actions-toolbar.active {
display: flex;
}
.bulk-actions-toolbar .btn {
color: white;
border-color: rgba(255,255,255,0.3);
}
.email-checkbox {
margin-right: 0.5rem;
}
< / style >
{% endblock %}
{% block content %}
< div class = "d-flex justify-content-between align-items-center mb-4" >
< div >
< h2 class = "fw-bold mb-1" > Email< / h2 >
< p class = "text-muted mb-0" > Administrer og klassificer emails automatisk< / p >
< / div >
< div class = "d-flex gap-2" >
< button class = "btn btn-light border" onclick = "showRulesModal()" title = "Email Rules" >
< i class = "bi bi-gear me-2" > < / i > Regler
< / button >
< button class = "btn btn-primary" onclick = "manualProcessEmails()" id = "processBtn" >
< i class = "bi bi-arrow-clockwise me-2" > < / i > Hent Nye
< / button >
< / div >
< / div >
<!-- Bulk Actions Toolbar -->
< div class = "bulk-actions-toolbar" id = "bulkActionsToolbar" >
< input type = "checkbox" id = "selectAllCheckbox" onchange = "toggleSelectAll()" >
< span id = "selectedCount" > 0< / span > valgt
< div class = "ms-auto d-flex gap-2" >
< button class = "btn btn-sm" onclick = "bulkArchive()" >
< i class = "bi bi-archive me-1" > < / i > Arkivér
< / button >
< button class = "btn btn-sm" onclick = "bulkReprocess()" >
< i class = "bi bi-arrow-clockwise me-1" > < / i > Genbehandl
< / button >
< button class = "btn btn-sm" onclick = "bulkDelete()" >
< i class = "bi bi-trash me-1" > < / i > Slet
< / button >
< button class = "btn btn-sm" onclick = "clearSelection()" >
< i class = "bi bi-x-lg" > < / i >
< / button >
< / div >
< / div >
<!-- Main 3 - Column Layout -->
< div class = "email-container" >
<!-- Left Sidebar - Email List -->
< div class = "email-list-sidebar" >
< div class = "email-list-header" >
< input type = "text"
class="form-control"
id="emailSearchInput"
placeholder="Søg emails..."
style="border-radius: 20px;">
< / div >
< div class = "email-list-filters" >
< button class = "filter-pill active" data-filter = "all" onclick = "setFilter('all')" >
Alle < span class = "count" id = "countAll" > 0< / span >
< / button >
< button class = "filter-pill" data-filter = "invoice" onclick = "setFilter('invoice')" >
Faktura < span class = "count" id = "countInvoice" > 0< / span >
< / button >
< button class = "filter-pill" data-filter = "order_confirmation" onclick = "setFilter('order_confirmation')" >
Ordre < span class = "count" id = "countOrder" > 0< / span >
< / button >
< button class = "filter-pill" data-filter = "freight_note" onclick = "setFilter('freight_note')" >
Fragt < span class = "count" id = "countFreight" > 0< / span >
< / button >
< button class = "filter-pill" data-filter = "time_confirmation" onclick = "setFilter('time_confirmation')" >
Tid < span class = "count" id = "countTime" > 0< / span >
< / button >
< button class = "filter-pill" data-filter = "case_notification" onclick = "setFilter('case_notification')" >
Sag < span class = "count" id = "countCase" > 0< / span >
< / button >
< button class = "filter-pill" data-filter = "general" onclick = "setFilter('general')" >
Generel < span class = "count" id = "countGeneral" > 0< / span >
< / button >
< button class = "filter-pill" data-filter = "spam" onclick = "setFilter('spam')" >
Spam < span class = "count" id = "countSpam" > 0< / span >
< / button >
< / div >
< div class = "email-list-body" id = "emailListBody" >
< div class = "loading-spinner" >
< div class = "spinner-border text-primary" role = "status" >
< span class = "visually-hidden" > Loading...< / span >
< / div >
< / div >
< / div >
< / div >
<!-- Center Pane - Email Content -->
< div class = "email-content-pane" id = "emailContentPane" >
< div class = "empty-state" >
< i class = "bi bi-envelope" > < / i >
< p > Vælg en email for at se indholdet< / p >
< small class = "text-muted" > Brug ↑↓ eller j/k til at navigere< / small >
< / div >
< / div >
<!-- Right Sidebar - AI Analysis -->
< div class = "email-analysis-sidebar" id = "emailAnalysisSidebar" >
< div class = "analysis-card" >
< h6 > < i class = "bi bi-robot me-2" > < / i > AI Klassificering< / h6 >
< p class = "text-muted small mb-3" > Vælg en email for at se analyse< / p >
< / div >
< / div >
< / div >
<!-- Keyboard Shortcuts Hint -->
< div class = "keyboard-hint" >
Tryk < kbd > ?< / kbd > for genveje
< / div >
<!-- Email Rules Modal -->
< div class = "modal fade" id = "emailRulesModal" tabindex = "-1" >
< div class = "modal-dialog modal-lg" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Email Regler< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< div class = "mb-3" >
< button class = "btn btn-primary" onclick = "showCreateRuleForm()" >
< i class = "bi bi-plus-lg me-2" > < / i > Ny Regel
< / button >
< / div >
< div class = "table-responsive" >
< table class = "table table-hover" >
< thead >
< tr >
< th > Prioritet< / th >
< th > Navn< / th >
< th > Handling< / th >
< th > Status< / th >
< th > Handlinger< / th >
< / tr >
< / thead >
< tbody id = "rulesTableBody" >
< tr >
< td colspan = "5" class = "text-center py-4" >
< div class = "spinner-border spinner-border-sm text-primary" > < / div >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Keyboard Shortcuts Help Modal -->
< div class = "modal fade" id = "shortcutsModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" > Keyboard Shortcuts< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< div class = "row g-3" >
< div class = "col-12" >
< h6 class = "text-muted" > Navigation< / h6 >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > j< / kbd > eller < kbd > ↓< / kbd > < / span >
< span class = "text-muted" > Næste email< / span >
< / div >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > k< / kbd > eller < kbd > ↑< / kbd > < / span >
< span class = "text-muted" > Forrige email< / span >
< / div >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > Enter< / kbd > < / span >
< span class = "text-muted" > Åbn email< / span >
< / div >
< / div >
< div class = "col-12" >
< h6 class = "text-muted" > Handlinger< / h6 >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > e< / kbd > < / span >
< span class = "text-muted" > Arkivér< / span >
< / div >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > r< / kbd > < / span >
< span class = "text-muted" > Genbehandl< / span >
< / div >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > c< / kbd > < / span >
< span class = "text-muted" > Skift klassificering< / span >
< / div >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > x< / kbd > < / span >
< span class = "text-muted" > Vælg/fravælg< / span >
< / div >
< / div >
< div class = "col-12" >
< h6 class = "text-muted" > Andet< / h6 >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > /< / kbd > eller < kbd > Cmd+K< / kbd > < / span >
< span class = "text-muted" > Søg< / span >
< / div >
< div class = "d-flex justify-content-between mb-2" >
< span > < kbd > Esc< / kbd > < / span >
< span class = "text-muted" > Luk/fravælg< / span >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Attachment Preview Modal -->
< div class = "modal fade" id = "attachmentPreviewModal" tabindex = "-1" data-bs-backdrop = "true" >
< div class = "modal-dialog modal-xl modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" id = "attachmentPreviewTitle" > Vedhæftning< / h5 >
< div class = "ms-auto d-flex gap-2" >
< a id = "attachmentDownloadBtn" href = "#" download class = "btn btn-sm btn-primary" >
< i class = "bi bi-download me-1" > < / i > Download
< / a >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< / div >
< div class = "modal-body p-0" style = "min-height: 70vh; max-height: 80vh; overflow: auto;" >
< div id = "attachmentPreviewContent" class = "w-100 h-100 d-flex align-items-center justify-content-center" >
< div class = "spinner-border text-primary" role = "status" >
< span class = "visually-hidden" > Indlæser...< / span >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
{% endblock %}
{% block extra_js %}
< script >
console.log('🚀 Email UI JavaScript loaded');
// State Management
let currentFilter = 'all';
let currentEmailId = null;
let emails = [];
let selectedEmails = new Set();
let emailSearchTimeout = null;
let autoRefreshInterval = null;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('📧 Email UI: DOMContentLoaded fired');
console.log('Email list element:', document.getElementById('emailListBody'));
loadEmails();
loadStats();
setupEventListeners();
setupKeyboardShortcuts();
startAutoRefresh();
});
// Setup Event Listeners
function setupEventListeners() {
document.getElementById('emailSearchInput').addEventListener('input', (e) => {
clearTimeout(emailSearchTimeout);
emailSearchTimeout = setTimeout(() => {
loadEmails(e.target.value);
}, 300);
});
}
// Keyboard Shortcuts
function setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
if (e.key === 'Escape') e.target.blur();
return;
}
switch(e.key) {
case 'j':
case 'ArrowDown':
e.preventDefault();
navigateEmails('next');
break;
case 'k':
case 'ArrowUp':
e.preventDefault();
navigateEmails('prev');
break;
case 'Enter':
if (currentEmailId) loadEmailDetail(currentEmailId);
break;
case 'e':
if (currentEmailId) archiveEmail();
break;
case 'r':
if (currentEmailId) reprocessEmail();
break;
case 'c':
if (currentEmailId) document.getElementById('classificationSelect')?.focus();
break;
case 'x':
if (currentEmailId) toggleEmailSelection(currentEmailId);
break;
case '/':
e.preventDefault();
document.getElementById('emailSearchInput').focus();
break;
case 'Escape':
clearSelection();
currentEmailId = null;
showEmptyState();
break;
case '?':
new bootstrap.Modal(document.getElementById('shortcutsModal')).show();
break;
}
});
}
function navigateEmails(direction) {
if (emails.length === 0) return;
let currentIndex = emails.findIndex(e => e.id === currentEmailId);
if (currentIndex === -1) {
currentIndex = 0;
} else if (direction === 'next' & & currentIndex < emails.length - 1 ) {
currentIndex++;
} else if (direction === 'prev' & & currentIndex > 0) {
currentIndex--;
} else {
return;
}
const email = emails[currentIndex];
selectEmail(email.id);
const emailElement = document.querySelector(`[data-email-id="${email.id}"]`);
if (emailElement) {
emailElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
async function loadEmails(searchQuery = '') {
try {
let url = '/api/v1/emails?limit=100';
if (currentFilter !== 'all') {
url += `&classification=${currentFilter}`;
}
if (searchQuery) {
url += `&q=${encodeURIComponent(searchQuery)}`;
}
console.log('Loading emails from:', url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
emails = await response.json();
console.log('Loaded emails:', emails.length, 'items');
renderEmailList(emails);
} catch (error) {
console.error('Failed to load emails:', error);
showError('Kunne ikke indlæse emails: ' + error.message);
}
}
function renderEmailList(emailList) {
const tbody = document.getElementById('emailListBody');
console.log('Rendering email list:', emailList.length, 'emails');
if (!emailList || emailList.length === 0) {
tbody.innerHTML = `
< div class = "empty-state" >
< i class = "bi bi-inbox" > < / i >
< p > Ingen emails fundet< / p >
< / div >
`;
return;
}
try {
tbody.innerHTML = emailList.map(email => {
const initials = getInitials(email.sender_name || email.sender_email);
const timeAgo = formatTimeAgo(email.received_date);
const preview = (email.body_text || '').substring(0, 80);
const unreadClass = email.is_read ? '' : 'unread';
const activeClass = email.id === currentEmailId ? 'active' : '';
const classification = email.classification || 'general';
return `
< div class = "email-item ${unreadClass} ${activeClass}"
data-email-id="${email.id}"
onclick="selectEmail(${email.id})">
< input type = "checkbox"
class="email-checkbox"
${selectedEmails.has(email.id) ? 'checked' : ''}
onclick="event.stopPropagation(); toggleEmailSelection(${email.id})">
< div class = "sender-avatar" > ${initials}< / div >
< div class = "email-item-content" >
< div class = "email-item-header" >
< div class = "email-sender" > ${email.sender_name || email.sender_email}< / div >
< div class = "email-time" > ${timeAgo}< / div >
< / div >
< div class = "email-subject" > ${escapeHtml(email.subject || '(Ingen emne)')}< / div >
< div class = "email-preview" > ${escapeHtml(preview)}< / div >
< div class = "email-meta" >
${!email.is_read ? '< span class = "unread-indicator" > < / span > ' : ''}
< span class = "classification-badge classification-${classification}" >
${formatClassification(classification)}
< / span >
${email.confidence_score ? `< small class = "text-muted" > ${Math.round(email.confidence_score * 100)}%< / small > ` : ''}
${email.has_attachments ? `< span class = "text-muted ms-2" > < i class = "bi bi-paperclip" > < / i > ${email.attachment_count || ''}< / span > ` : ''}
< / div >
< / div >
< / div >
`;
}).join('');
console.log('Email list rendered successfully');
} catch (error) {
console.error('Error rendering email list:', error);
tbody.innerHTML = `
< div class = "empty-state" >
< i class = "bi bi-exclamation-triangle text-danger" > < / i >
< p > Fejl ved visning af emails< / p >
< small > ${error.message}< / small >
< / div >
`;
}
}
async function selectEmail(emailId) {
currentEmailId = emailId;
document.querySelectorAll('.email-item').forEach(item => {
item.classList.toggle('active', item.dataset.emailId == emailId);
});
await loadEmailDetail(emailId);
}
async function loadEmailDetail(emailId) {
try {
console.log('Loading email detail for ID:', emailId);
const response = await fetch(`/api/v1/emails/${emailId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const email = await response.json();
console.log('Email detail loaded:', email);
renderEmailDetail(email);
renderEmailAnalysis(email);
if (!email.is_read) {
await markAsRead(emailId);
}
} catch (error) {
console.error('Failed to load email detail:', error);
showError('Kunne ikke indlæse email detaljer: ' + error.message);
}
}
function getClassificationActions(email) {
const actions = [];
switch(email.classification) {
case 'invoice':
actions.push(`
< button class = "btn btn-primary w-100" onclick = "createSupplierInvoice(${email.id})" title = "Opret leverandørfaktura" >
< i class = "bi bi-receipt me-2" > < / i > Opret Leverandørfaktura
< / button >
`);
break;
case 'order_confirmation':
actions.push(`
< button class = "btn btn-success w-100" onclick = "createOrder(${email.id})" title = "Opret ordre" >
< i class = "bi bi-cart-check me-2" > < / i > Opret Ordre
< / button >
`);
break;
case 'freight_note':
actions.push(`
< button class = "btn btn-info w-100" onclick = "createFreightNote(${email.id})" title = "Registrer fragt" >
< i class = "bi bi-truck me-2" > < / i > Registrer Fragt
< / button >
`);
break;
case 'time_confirmation':
actions.push(`
< button class = "btn btn-warning w-100" onclick = "createTimeEntry(${email.id})" title = "Registrer tid" >
< i class = "bi bi-clock me-2" > < / i > Registrer Tid
< / button >
`);
break;
case 'case_notification':
actions.push(`
< button class = "btn btn-secondary w-100" onclick = "createCase(${email.id})" title = "Opret sag" >
< i class = "bi bi-folder me-2" > < / i > Opret Sag
< / button >
`);
break;
case 'customer_email':
actions.push(`
< button class = "btn btn-primary w-100" onclick = "linkToCustomer(${email.id})" title = "Link til kunde" >
< i class = "bi bi-person-lines-fill me-2" > < / i > Link til Kunde
< / button >
`);
break;
}
return actions.join('');
}
function renderEmailDetail(email) {
const pane = document.getElementById('emailContentPane');
const initials = getInitials(email.sender_name || email.sender_email);
const timestamp = formatDateTime(email.received_date);
const attachmentsHtml = email.attachments & & email.attachments.length > 0 ? `
< div class = "email-attachments" >
< h6 class = "mb-3" > < i class = "bi bi-paperclip me-2" > < / i > Vedhæftninger (${email.attachments.length})< / h6 >
${email.attachments.map(att => {
const canPreview = canPreviewFile(att.content_type);
return `
< div class = "attachment-item d-flex align-items-center" >
< i class = "bi bi-file-earmark-${getFileIcon(att.content_type)} me-2" > < / i >
< span class = "flex-grow-1" > ${att.filename}< / span >
< small class = "text-muted me-2" > ${formatFileSize(att.size_bytes || att.file_size)}< / small >
${canPreview ? `
< button onclick = "previewAttachment(${email.id}, ${att.id}, '${escapeHtml(att.filename)}', '${att.content_type}')"
class="btn btn-sm btn-outline-primary me-1" title="Se">
< i class = "bi bi-eye" > < / i >
< / button >
` : ''}
< a href = "/api/v1/emails/${email.id}/attachments/${att.id}"
class="btn btn-sm btn-outline-secondary"
download="${att.filename}"
title="Download">
< i class = "bi bi-download" > < / i >
< / a >
< / div >
`;
}).join('')}
< / div >
` : '';
pane.innerHTML = `
< div class = "email-content-header" >
< h1 class = "email-content-subject" > ${escapeHtml(email.subject || '(Ingen emne)')}< / h1 >
< div class = "email-content-meta" >
< div class = "sender-info" >
< div class = "sender-avatar" style = "width: 48px; height: 48px; font-size: 1rem;" > ${initials}< / div >
< div class = "sender-details" >
< div class = "sender-name" > ${email.sender_name || 'Ukendt afsender'}< / div >
< div class = "sender-email" > ${email.sender_email}< / div >
< / div >
< / div >
< div class = "email-timestamp ms-auto" >
< i class = "bi bi-clock me-1" > < / i > ${timestamp}
< / div >
< / div >
< / div >
2025-12-11 12:57:14 +01:00
< div class = "email-actions d-flex justify-content-between align-items-center" >
< div class = "d-flex gap-2" >
< button class = "btn btn-sm btn-light border" onclick = "archiveEmail()" title = "Arkivér (e)" >
< i class = "bi bi-archive" > < / i >
< / button >
< button class = "btn btn-sm btn-light border" onclick = "markAsSpam()" title = "Marker som spam" >
< i class = "bi bi-exclamation-triangle" > < / i >
< / button >
< button class = "btn btn-sm btn-light border" onclick = "reprocessEmail()" title = "Genbehandl (r)" >
< i class = "bi bi-arrow-clockwise" > < / i >
< / button >
< button class = "btn btn-sm btn-light border text-danger" onclick = "deleteEmail()" title = "Slet" >
< i class = "bi bi-trash" > < / i >
< / button >
< / div >
${email.attachments & & email.attachments.length > 0 ? `
< div class = "d-flex align-items-center gap-2" >
< span class = "text-muted" > < i class = "bi bi-paperclip me-1" > < / i > ${email.attachments.length} vedhæftning${email.attachments.length > 1 ? 'er' : ''}< / span >
${email.attachments.map(att => {
const canPreview = canPreviewFile(att.content_type);
return `
${canPreview ? `
< button onclick = "previewAttachment(${email.id}, ${att.id}, '${escapeHtml(att.filename)}', '${att.content_type}')"
class="btn btn-sm btn-outline-primary" title="Se ${att.filename}">
< i class = "bi bi-eye me-1" > < / i > ${att.filename}
< / button >
` : `
< a href = "/api/v1/emails/${email.id}/attachments/${att.id}"
class="btn btn-sm btn-outline-secondary"
download="${att.filename}"
title="Download ${att.filename}">
< i class = "bi bi-download me-1" > < / i > ${att.filename}
< / a >
`}
`;
}).join('')}
< / div >
` : ''}
2025-12-11 12:45:29 +01:00
< / div >
< div class = "email-body" >
${email.body_html ? `< iframe srcdoc = "${email.body_html.replace(/" / g , ' & quot ; ' ) } " > < / iframe > ` :
`< pre style = "white-space: pre-wrap; font-family: inherit;" > ${escapeHtml(email.body_text || 'Ingen indhold')}< / pre > `}
< / div >
`;
}
function renderEmailAnalysis(email) {
const sidebar = document.getElementById('emailAnalysisSidebar');
const classification = email.classification || 'general';
const confidence = email.confidence_score || 0;
sidebar.innerHTML = `
${getClassificationActions(email) ? `
< div class = "analysis-card" >
< h6 > < i class = "bi bi-lightning-charge-fill me-2" > < / i > Hurtig Action< / h6 >
< div class = "d-flex flex-column gap-2" >
${getClassificationActions(email)}
< / div >
< / div >
` : ''}
< div class = "analysis-card" >
< h6 > < i class = "bi bi-robot me-2" > < / i > AI Klassificering< / h6 >
< div class = "confidence-meter" >
< div class = "confidence-bar" >
< div class = "confidence-fill" style = "width: ${confidence * 100}%" > < / div >
< / div >
< div class = "confidence-label" >
< span > Sikkerhed< / span >
< span > < strong > ${Math.round(confidence * 100)}%< / strong > < / span >
< / div >
< / div >
< select class = "classification-select" id = "classificationSelect" onchange = "updateClassification()" >
< option value = "invoice" $ { classification = == ' invoice ' ? ' selected ' : ' ' } > 📄 Faktura< / option >
< option value = "order_confirmation" $ { classification = == ' order_confirmation ' ? ' selected ' : ' ' } > 📦 Ordrebekræftelse< / option >
< option value = "freight_note" $ { classification = == ' freight_note ' ? ' selected ' : ' ' } > 🚚 Fragtnote< / option >
< option value = "time_confirmation" $ { classification = == ' time_confirmation ' ? ' selected ' : ' ' } > ⏰ Tidsregistrering< / option >
< option value = "case_notification" $ { classification = == ' case_notification ' ? ' selected ' : ' ' } > 📋 Sagsnotifikation< / option >
< option value = "bankruptcy" $ { classification = == ' bankruptcy ' ? ' selected ' : ' ' } > ⚠️ Konkurs< / option >
< option value = "spam" $ { classification = == ' spam ' ? ' selected ' : ' ' } > 🚫 Spam< / option >
< option value = "general" $ { classification = == ' general ' ? ' selected ' : ' ' } > 📧 Generel< / option >
< / select >
< button class = "btn btn-sm btn-primary w-100" onclick = "saveClassification()" >
< i class = "bi bi-check-lg me-2" > < / i > Gem Klassificering
< / button >
< / div >
< div class = "analysis-card" >
< h6 > < i class = "bi bi-info-circle me-2" > < / i > Metadata< / h6 >
< ul class = "metadata-list" >
< li class = "metadata-item" >
< div class = "metadata-label" > Message ID< / div >
< div class = "metadata-value" style = "font-size: 0.7rem; word-break: break-all;" > ${email.message_id || 'N/A'}< / div >
< / li >
< li class = "metadata-item" >
< div class = "metadata-label" > Modtaget< / div >
< div class = "metadata-value" > ${formatDateTime(email.received_date)}< / div >
< / li >
< li class = "metadata-item" >
< div class = "metadata-label" > Status< / div >
< div class = "metadata-value" > ${email.status || 'new'}< / div >
< / li >
${email.extracted_invoice_number ? `
< li class = "metadata-item" >
< div class = "metadata-label" > Fakturanummer< / div >
< div class = "metadata-value" > ${email.extracted_invoice_number}< / div >
< / li >
` : ''}
${email.extracted_amount ? `
< li class = "metadata-item" >
< div class = "metadata-label" > Beløb< / div >
< div class = "metadata-value" > ${email.extracted_amount} ${email.extracted_currency || 'DKK'}< / div >
< / li >
` : ''}
< / ul >
< / div >
${email.matched_rules & & email.matched_rules.length > 0 ? `
< div class = "analysis-card" >
< h6 > < i class = "bi bi-lightning-charge me-2" > < / i > Matchede Regler< / h6 >
< div class = "rules-indicator" >
< i class = "bi bi-check-circle-fill" > < / i >
< span > ${email.matched_rules.length} regel(er) matchet< / span >
< / div >
< / div >
` : ''}
`;
}
function showEmptyState() {
document.getElementById('emailContentPane').innerHTML = `
< div class = "empty-state" >
< i class = "bi bi-envelope" > < / i >
< p > Vælg en email for at se indholdet< / p >
< small class = "text-muted" > Brug ↑↓ eller j/k til at navigere< / small >
< / div >
`;
document.getElementById('emailAnalysisSidebar').innerHTML = `
< div class = "analysis-card" >
< h6 > < i class = "bi bi-robot me-2" > < / i > AI Klassificering< / h6 >
< p class = "text-muted small mb-3" > Vælg en email for at se analyse< / p >
< / div >
`;
}
function setFilter(filter) {
currentFilter = filter;
document.querySelectorAll('.filter-pill').forEach(pill => {
pill.classList.toggle('active', pill.dataset.filter === filter);
});
loadEmails();
}
async function loadStats() {
try {
const response = await fetch('/api/v1/emails/stats/summary');
const stats = await response.json();
document.getElementById('countAll').textContent = stats.total_emails || 0;
document.getElementById('countInvoice').textContent = stats.invoices || 0;
document.getElementById('countOrder').textContent = 0;
document.getElementById('countFreight').textContent = 0;
document.getElementById('countTime').textContent = stats.time_confirmations || 0;
document.getElementById('countCase').textContent = 0;
document.getElementById('countGeneral').textContent = stats.total_emails - stats.invoices - stats.time_confirmations || 0;
document.getElementById('countSpam').textContent = stats.spam_emails || 0;
} catch (error) {
console.error('Failed to load stats:', error);
}
}
async function markAsRead(emailId) {
try {
await fetch(`/api/v1/emails/${emailId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_read: true })
});
} catch (error) {
console.error('Failed to mark as read:', error);
}
}
async function archiveEmail() {
if (!currentEmailId) return;
try {
const response = await fetch(`/api/v1/emails/${currentEmailId}?status=archived`, {
method: 'PUT'
});
if (!response.ok) throw new Error('Archive failed');
showSuccess('Email arkiveret');
currentEmailId = null;
document.getElementById('emailContentPane').innerHTML = '< div class = "empty-state" > < i class = "bi bi-inbox" > < / i > < p > Vælg en email for at se indholdet< / p > < / div > ';
document.getElementById('emailAnalysisSidebar').innerHTML = '';
loadEmails();
} catch (error) {
showError('Kunne ikke arkivere email');
}
}
async function markAsSpam() {
if (!currentEmailId) return;
if (!confirm('Marker denne email som spam?')) return;
try {
const response = await fetch(`/api/v1/emails/${currentEmailId}/classify`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ classification: 'spam', confidence: 1.0 })
});
if (!response.ok) throw new Error('Classify failed');
showSuccess('Markeret som spam');
currentEmailId = null;
document.getElementById('emailContentPane').innerHTML = '< div class = "empty-state" > < i class = "bi bi-inbox" > < / i > < p > Vælg en email for at se indholdet< / p > < / div > ';
document.getElementById('emailAnalysisSidebar').innerHTML = '';
loadEmails();
} catch (error) {
showError('Kunne ikke markere som spam');
}
}
async function reprocessEmail() {
if (!currentEmailId) return;
try {
const btn = event.target.closest('button');
btn.disabled = true;
btn.innerHTML = '< span class = "spinner-border spinner-border-sm" > < / span > ';
const response = await fetch(`/api/v1/emails/${currentEmailId}/reprocess`, {
method: 'POST'
});
if (!response.ok) throw new Error('Reprocess failed');
const result = await response.json();
showSuccess(`Genbehandlet: ${result.classification} (${Math.round(result.confidence * 100)}%)`);
await loadEmailDetail(currentEmailId);
loadEmails();
} catch (error) {
showError('Kunne ikke genbehandle email');
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '< i class = "bi bi-arrow-clockwise" > < / i > ';
}
}
}
async function deleteEmail() {
if (!currentEmailId) return;
if (!confirm('Slet denne email permanent?')) return;
try {
const response = await fetch(`/api/v1/emails/${currentEmailId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Delete failed');
showSuccess('Email slettet');
currentEmailId = null;
showEmptyState();
loadEmails();
} catch (error) {
showError('Kunne ikke slette email');
}
}
// Classification Action Handlers
async function createSupplierInvoice(emailId) {
try {
// Get email data to pre-fill form
const response = await fetch(`/api/v1/emails/${emailId}`);
if (!response.ok) throw new Error('Failed to fetch email');
const email = await response.json();
// Store email context in sessionStorage
sessionStorage.setItem('supplierInvoiceContext', JSON.stringify({
emailId: emailId,
subject: email.subject,
sender: email.sender_email,
attachments: email.attachments
}));
// Redirect to supplier invoice page (it will auto-open modal)
window.location.href = '/billing/supplier-invoices';
} catch (error) {
showError('Kunne ikke åbne leverandørfaktura formular');
}
}
async function createOrder(emailId) {
showError('Ordre-modul er ikke implementeret endnu');
}
async function createFreightNote(emailId) {
showError('Fragt-modul er ikke implementeret endnu');
}
async function createTimeEntry(emailId) {
showError('Tidsregistrering-modul er ikke implementeret endnu');
}
async function createCase(emailId) {
showError('Sags-modul er ikke implementeret endnu');
}
async function linkToCustomer(emailId) {
showError('Kunde-linking er ikke implementeret endnu');
}
async function saveClassification() {
if (!currentEmailId) return;
const classification = document.getElementById('classificationSelect').value;
try {
await fetch(`/api/v1/emails/${currentEmailId}/classify`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
classification: classification,
confidence: 0.9
})
});
showSuccess('Klassificering gemt');
loadEmails();
loadEmailDetail(currentEmailId);
} catch (error) {
showError('Kunne ikke gemme klassificering');
}
}
async function manualProcessEmails() {
const btn = document.getElementById('processBtn');
btn.disabled = true;
btn.innerHTML = '< span class = "spinner-border spinner-border-sm me-2" > < / span > Henter...';
try {
const response = await fetch('/api/v1/emails/process', {
method: 'POST'
});
const result = await response.json();
showSuccess(`Hentet ${result.stats.fetched} nye emails`);
loadEmails();
loadStats();
} catch (error) {
showError('Kunne ikke hente emails');
} finally {
btn.disabled = false;
btn.innerHTML = '< i class = "bi bi-arrow-clockwise me-2" > < / i > Hent Nye';
}
}
function toggleEmailSelection(emailId) {
if (selectedEmails.has(emailId)) {
selectedEmails.delete(emailId);
} else {
selectedEmails.add(emailId);
}
updateBulkToolbar();
renderEmailList(emails);
}
function toggleSelectAll() {
const checkbox = document.getElementById('selectAllCheckbox');
if (checkbox.checked) {
emails.forEach(email => selectedEmails.add(email.id));
} else {
selectedEmails.clear();
}
updateBulkToolbar();
renderEmailList(emails);
}
function clearSelection() {
selectedEmails.clear();
document.getElementById('selectAllCheckbox').checked = false;
updateBulkToolbar();
renderEmailList(emails);
}
function updateBulkToolbar() {
const toolbar = document.getElementById('bulkActionsToolbar');
const count = selectedEmails.size;
if (count > 0) {
toolbar.classList.add('active');
document.getElementById('selectedCount').textContent = count;
} else {
toolbar.classList.remove('active');
}
}
async function bulkArchive() {
if (selectedEmails.size === 0) return;
const count = selectedEmails.size;
try {
const response = await fetch('/api/v1/emails/bulk/archive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([...selectedEmails])
});
if (!response.ok) throw new Error('Bulk archive failed');
showSuccess(`${count} emails arkiveret`);
clearSelection();
loadEmails();
} catch (error) {
showError('Kunne ikke arkivere emails');
}
}
async function bulkReprocess() {
if (selectedEmails.size === 0) return;
const count = selectedEmails.size;
try {
const response = await fetch('/api/v1/emails/bulk/reprocess', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([...selectedEmails])
});
if (!response.ok) throw new Error('Bulk reprocess failed');
showSuccess(`${count} emails genbehandlet`);
clearSelection();
loadEmails();
} catch (error) {
showError('Kunne ikke genbehandle emails');
}
}
async function bulkDelete() {
if (selectedEmails.size === 0) return;
const count = selectedEmails.size;
if (!confirm(`Slet ${count} emails permanent?`)) return;
try {
const response = await fetch('/api/v1/emails/bulk/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([...selectedEmails])
});
if (!response.ok) throw new Error('Bulk delete failed');
showSuccess(`${count} emails slettet`);
clearSelection();
loadEmails();
} catch (error) {
showError('Kunne ikke slette emails');
}
}
async function showRulesModal() {
const modal = new bootstrap.Modal(document.getElementById('emailRulesModal'));
modal.show();
try {
const response = await fetch('/api/v1/email-rules');
const rules = await response.json();
const tbody = document.getElementById('rulesTableBody');
tbody.innerHTML = rules.map(rule => `
< tr >
< td > < span class = "badge bg-secondary" > ${rule.priority}< / span > < / td >
< td > ${rule.name}< / td >
< td > ${rule.action_type}< / td >
< td >
< span class = "badge bg-${rule.enabled ? 'success' : 'secondary'}" >
${rule.enabled ? 'Aktiv' : 'Inaktiv'}
< / span >
< / td >
< td >
< button class = "btn btn-sm btn-light border" onclick = "editRule(${rule.id})" >
< i class = "bi bi-pencil" > < / i >
< / button >
< button class = "btn btn-sm btn-light border text-danger" onclick = "deleteRule(${rule.id})" >
< i class = "bi bi-trash" > < / i >
< / button >
< / td >
< / tr >
`).join('');
} catch (error) {
console.error('Failed to load rules:', error);
}
}
function startAutoRefresh() {
autoRefreshInterval = setInterval(() => {
loadEmails();
loadStats();
}, 30000);
}
function getInitials(name) {
if (!name) return '?';
const parts = name.split(/[\s@]+/);
return parts.slice(0, 2).map(p => p[0].toUpperCase()).join('');
}
function formatTimeAgo(dateString) {
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
if (seconds < 60 ) return ' Lige nu ' ;
if (seconds < 3600 ) return ` $ { Math . floor ( seconds / 60 ) } m ` ;
if (seconds < 86400 ) return ` $ { Math . floor ( seconds / 3600 ) } t ` ;
if (seconds < 604800 ) return ` $ { Math . floor ( seconds / 86400 ) } d ` ;
return date.toLocaleDateString('da-DK', { day: 'numeric', month: 'short' });
}
function formatDateTime(dateString) {
const date = new Date(dateString);
return date.toLocaleString('da-DK', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function formatClassification(classification) {
const map = {
'invoice': 'Faktura',
'order_confirmation': 'Ordre',
'freight_note': 'Fragt',
'time_confirmation': 'Tid',
'case_notification': 'Sag',
'bankruptcy': 'Konkurs',
'spam': 'Spam',
'general': 'Generel'
};
return map[classification] || classification;
}
function getFileIcon(contentType) {
if (contentType?.includes('pdf')) return 'pdf';
if (contentType?.includes('image')) return 'image';
if (contentType?.includes('zip') || contentType?.includes('compressed')) return 'zip';
if (contentType?.includes('word') || contentType?.includes('document')) return 'word';
if (contentType?.includes('excel') || contentType?.includes('spreadsheet')) return 'excel';
return 'text';
}
function formatFileSize(bytes) {
if (!bytes) return 'N/A';
if (bytes < 1024 ) return bytes + ' B ' ;
if (bytes < 1048576 ) return ( bytes / 1024 ) . toFixed ( 1 ) + ' KB ' ;
return (bytes / 1048576).toFixed(1) + ' MB';
}
function canPreviewFile(contentType) {
if (!contentType) return false;
const previewable = [
'application/pdf',
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp',
'text/plain',
'text/html'
];
return previewable.some(type => contentType.toLowerCase().includes(type.toLowerCase()));
}
function previewAttachment(emailId, attachmentId, filename, contentType) {
const modal = new bootstrap.Modal(document.getElementById('attachmentPreviewModal'));
const titleEl = document.getElementById('attachmentPreviewTitle');
const contentEl = document.getElementById('attachmentPreviewContent');
const downloadBtn = document.getElementById('attachmentDownloadBtn');
// Set title and download link
titleEl.textContent = filename;
downloadBtn.href = `/api/v1/emails/${emailId}/attachments/${attachmentId}`;
downloadBtn.download = filename;
// Show loading spinner
contentEl.innerHTML = `
< div class = "spinner-border text-primary" role = "status" >
< span class = "visually-hidden" > Indlæser...< / span >
< / div >
`;
// Show modal
modal.show();
// Build preview based on content type
const url = `/api/v1/emails/${emailId}/attachments/${attachmentId}`;
if (contentType.includes('pdf')) {
// PDF preview using embed
contentEl.innerHTML = `
< embed src = "${url}"
type="application/pdf"
width="100%"
height="100%"
style="min-height: 70vh;" />
`;
} else if (contentType.startsWith('image/')) {
// Image preview
contentEl.innerHTML = `
< img src = "${url}"
alt="${escapeHtml(filename)}"
style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />
`;
} else if (contentType.includes('text/')) {
// Text preview
fetch(url)
.then(response => response.text())
.then(text => {
contentEl.innerHTML = `
< pre style = "padding: 2rem; margin: 0; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; text-align: left;" > ${escapeHtml(text)}< / pre >
`;
})
.catch(error => {
contentEl.innerHTML = `
< div class = "alert alert-danger m-4" >
< i class = "bi bi-exclamation-triangle me-2" > < / i >
Kunne ikke indlæse fil: ${error.message}
< / div >
`;
});
} else {
contentEl.innerHTML = `
< div class = "alert alert-info m-4" >
< i class = "bi bi-info-circle me-2" > < / i >
Denne filtype kan ikke vises. Brug download-knappen.
< / div >
`;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showSuccess(message) {
alert(message);
}
function showError(message) {
alert('Fejl: ' + message);
}
window.addEventListener('beforeunload', () => {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
});
< / script >
{% endblock %}