bmc_hub/app/emails/frontend/emails.html

1798 lines
59 KiB
HTML
Raw Normal View History

{% 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);
flex-wrap: wrap;
}
.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>
<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>
` : ''}
</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 %}