1798 lines
59 KiB
HTML
1798 lines
59 KiB
HTML
{% 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, '"')}"></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 %}
|