bmc_hub/app/billing/frontend/supplier_invoices.html

5280 lines
228 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "shared/frontend/base.html" %}
{% block title %}Leverandør fakturaer - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.stat-card {
text-align: center;
padding: 1.5rem;
}
.stat-card h3 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0;
}
.stat-card p {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 0;
}
.stat-card.overdue h3 { color: var(--danger); }
.stat-card.due-soon h3 { color: var(--warning); }
.stat-card.pending h3 { color: var(--accent); }
.stat-card.total h3 { color: var(--text-secondary); }
.badge {
padding: 0.4rem 0.8rem;
border-radius: 6px;
font-weight: 500;
}
.status-pending { background-color: #ffc107; color: #000; }
.status-approved { background-color: #17a2b8; color: #fff; }
.status-sent { background-color: var(--success); color: #fff; }
.status-paid { background-color: #6c757d; color: #fff; }
.status-overdue { background-color: var(--danger); color: #fff; }
.status-unpaid { background-color: #ffc107; color: #000; }
.line-item {
background: var(--accent-light);
padding: 1rem;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.filter-pills {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.filter-pill {
padding: 0.5rem 1rem;
border-radius: 20px;
border: 1px solid #dee2e6;
background: var(--bg-card);
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
.filter-pill:hover, .filter-pill.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
/* Tab Navigation */
.nav-tabs {
border-bottom: 2px solid #dee2e6;
}
.nav-tabs .nav-link {
color: var(--text-secondary);
border: none;
border-bottom: 3px solid transparent;
padding: 0.75rem 1.5rem;
font-weight: 500;
transition: all 0.2s;
}
.nav-tabs .nav-link:hover {
border-color: var(--accent-light);
color: var(--accent);
}
.nav-tabs .nav-link.active {
color: var(--accent);
border-bottom-color: var(--accent);
background: none;
}
/* Pending Files Status Badges */
.status-ai_extracted { background-color: #17a2b8; color: #fff; }
.status-processing { background-color: #6c757d; color: #fff; }
.status-failed { background-color: var(--danger); color: #fff; }
.status-completed { background-color: var(--success); color: #fff; }
</style>
{% endblock %}
{% block content %}
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-1">📋 Leverandørfakturaer</h2>
<p class="text-muted mb-0">Kassekladde - Integration med e-conomic</p>
</div>
<div>
<a href="/billing/templates" class="btn btn-outline-secondary me-2">
<i class="bi bi-grid-3x3 me-2"></i>Se Templates
</a>
<a href="/billing/template-builder" class="btn btn-outline-primary me-2">
<i class="bi bi-puzzle me-2"></i>Template Builder
</a>
<button class="btn btn-success" onclick="openUploadModal()">
<i class="bi bi-cloud-upload me-2"></i>Upload Faktura
</button>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4" id="statsCards">
<div class="col-md-3">
<div class="card stat-card overdue">
<h3 id="statOverdueCount">-</h3>
<p>Overskredet</p>
<small class="text-muted" id="statOverdueAmount">-</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card due-soon">
<h3 id="statDueSoonCount">-</h3>
<p>Forfald inden 7 dage</p>
<small class="text-muted" id="statDueSoonAmount">-</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card pending">
<h3 id="statPendingCount">-</h3>
<p>Afventer behandling</p>
<small class="text-muted" id="statPendingAmount">-</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card total">
<h3 id="statTotalCount">-</h3>
<p>Ubetalt i alt</p>
<small class="text-muted" id="statUnpaidAmount">-</small>
</div>
</div>
</div>
<!-- Tab Navigation -->
<ul class="nav nav-tabs mb-4" id="mainTabs">
<li class="nav-item">
<a class="nav-link active" id="unhandled-tab" data-bs-toggle="tab" href="#unhandled-content" onclick="switchToUnhandledTab()">
<i class="bi bi-inbox me-2"></i>Ubehandlede Fakturaer
<span class="badge bg-warning text-dark ms-2" id="unhandledCount" style="display: none;">0</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="kassekladde-tab" data-bs-toggle="tab" href="#kassekladde-content" onclick="switchToKassekladdeTab()">
<i class="bi bi-journal-text me-2"></i>Kassekladde
<span class="badge bg-primary ms-2" id="kassekladdeCount" style="display: none;">0</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="payment-tab" data-bs-toggle="tab" href="#payment-content" onclick="switchToPaymentTab()">
<i class="bi bi-calendar-check me-2"></i>Til Betaling
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="ready-tab" data-bs-toggle="tab" href="#ready-content" onclick="switchToReadyTab()">
<i class="bi bi-check-circle me-2"></i>Klar til Bogføring
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="lines-tab" data-bs-toggle="tab" href="#lines-content" onclick="switchToLinesTab()">
<i class="bi bi-list-ul me-2"></i>Varelinjer
</a>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content" id="mainTabContent">
<!-- Ubehandlede Fakturaer Tab -->
<div class="tab-pane fade show active" id="unhandled-content">
<div class="alert alert-info mb-4">
<i class="bi bi-inbox me-2"></i>
<strong>Ubehandlede Fakturaer:</strong> PDFer der venter på analyse og vendor-matching. Klik "Analyser alle" for at køre automatisk extraction.
</div>
<!-- Batch Actions Bar -->
<div class="mb-3">
<button class="btn btn-primary" onclick="batchAnalyzeAllFiles()">
<i class="bi bi-lightning me-2"></i>Analyser alle
</button>
<button class="btn btn-outline-secondary ms-2" onclick="loadUnhandledFiles()">
<i class="bi bi-arrow-clockwise me-2"></i>Opdater
</button>
</div>
<!-- Unhandled Files Table -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Filnavn</th>
<th>Dato</th>
<th>Leverandør-forslag</th>
<th>Confidence</th>
<th>Beløb</th>
<th>Status</th>
<th>Handlinger</th>
</tr>
</thead>
<tbody id="unhandledTable">
<tr>
<td colspan="7" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Kassekladde Tab -->
<div class="tab-pane fade" id="kassekladde-content">
<div class="alert alert-success mb-4">
<i class="bi bi-journal-text me-2"></i>
<strong>Kassekladde:</strong> Fakturaer med momskoder og modkonti. Klar til gennemgang og manuel afsendelse til e-conomic.
</div>
<!-- Bulk Actions Bar for Kassekladde -->
<div class="alert alert-light border mb-3" id="kassekladdeBulkActionsBar" style="display: none;">
<div class="d-flex align-items-center justify-content-between">
<div>
<strong><span id="selectedKassekladdeCount">0</span> fakturaer valgt</strong>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-success" onclick="bulkSendToEconomicKassekladde()" title="Send til e-conomic kassekladde">
<i class="bi bi-send me-1"></i>Send til e-conomic
</button>
</div>
</div>
</div>
<!-- Kassekladde Table -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" class="form-check-input" id="selectAllKassekladde" onchange="toggleSelectAllKassekladde()">
</th>
<th>Fakturanr.</th>
<th>Leverandør</th>
<th>Fakturadato</th>
<th>Forfald</th>
<th>Beløb</th>
<th>Linjer</th>
<th>Status</th>
<th>Handlinger</th>
</tr>
</thead>
<tbody id="kassekladdeTable">
<tr>
<td colspan="9" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Til Betaling Tab -->
<div class="tab-pane fade" id="payment-content">
<div class="alert alert-info mb-4">
<i class="bi bi-info-circle me-2"></i>
<strong>Til Betaling:</strong> Fakturaer sorteret efter forfaldsdato. Brug checkboxes til at vælge hvilke der skal betales.
</div>
<!-- Bulk Actions Bar for Payment -->
<div class="alert alert-light border mb-3" id="paymentBulkActionsBar" style="display: none;">
<div class="d-flex align-items-center justify-content-between">
<div>
<strong><span id="selectedPaymentCount">0</span> fakturaer valgt</strong>
<span class="text-muted ms-3">Total: <span id="selectedPaymentTotal">0</span> kr</span>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-success" onclick="bulkMarkAsPaid()" title="Marker som betalt">
<i class="bi bi-cash-coin me-1"></i>Marker Betalt
</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="exportPaymentFile()" title="Eksporter til betalingsfil">
<i class="bi bi-download me-1"></i>Eksporter
</button>
</div>
</div>
</div>
<!-- Payment Table -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" class="form-check-input" id="selectAllPayment" onchange="toggleSelectAllPayment()">
</th>
<th>Forfaldsdato</th>
<th>Fakturanr.</th>
<th>Leverandør</th>
<th>Beløb</th>
<th>Status</th>
<th>Handlinger</th>
</tr>
</thead>
<tbody id="paymentTable">
<tr>
<td colspan="7" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Klar til Bogføring Tab -->
<div class="tab-pane fade" id="ready-content">
<div class="alert alert-success mb-4">
<i class="bi bi-check-circle me-2"></i>
<strong>Klar til Bogføring:</strong> Fakturaer hvor alle varelinjer har modkonto og momskode. Klar til afsendelse til e-conomic.
</div>
<!-- Bulk Actions Bar for Ready -->
<div class="alert alert-light border mb-3" id="readyBulkActionsBar" style="display: none;">
<div class="d-flex align-items-center justify-content-between">
<div>
<strong><span id="selectedReadyCount">0</span> fakturaer valgt</strong>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-primary" onclick="bulkSendToEconomic()" title="Send til e-conomic kassekladde">
<i class="bi bi-send me-1"></i>Send til e-conomic
</button>
</div>
</div>
</div>
<!-- Ready Table --><div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" class="form-check-input" id="selectAllReady" onchange="toggleSelectAllReady()">
</th>
<th>Fakturanr.</th>
<th>Leverandør</th>
<th>Fakturadato</th>
<th>Beløb</th>
<th>Linjer</th>
<th>Handlinger</th>
</tr>
</thead>
<tbody id="readyTable">
<tr>
<td colspan="7" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Varelinjer Tab -->
<div class="tab-pane fade" id="lines-content">
<div class="alert alert-warning mb-4">
<i class="bi bi-list-ul me-2"></i>
<strong>Varelinjer Tracking:</strong> Overblik over alle varelinjer. Sæt formål og spor viderefakturering.
</div>
<!-- Filters for Lines -->
<div class="card mb-3">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label small">Formål</label>
<select class="form-select form-select-sm" id="linePurposeFilter" onchange="loadLineItems()">
<option value="">Alle</option>
<option value="resale">Videresalg</option>
<option value="internal">Internt brug</option>
<option value="project">Projekt</option>
<option value="stock">Lager</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label small">Status</label>
<select class="form-select form-select-sm" id="lineStatusFilter" onchange="loadLineItems()">
<option value="">Alle</option>
<option value="not_invoiced">Ikke viderefaktureret</option>
<option value="invoiced">Viderefaktureret</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label small">Leverandør</label>
<input type="text" class="form-control form-control-sm" id="lineVendorFilter" placeholder="Søg leverandør..." onkeyup="loadLineItems()">
</div>
<div class="col-md-3">
<label class="form-label small">&nbsp;</label>
<button class="btn btn-sm btn-outline-secondary w-100" onclick="clearLineFilters()">
<i class="bi bi-x-circle me-1"></i>Ryd filtre
</button>
</div>
</div>
</div>
</div>
<!-- Lines Table -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-sm">
<thead>
<tr>
<th>Fakturanr.</th>
<th>Leverandør</th>
<th>Beskrivelse</th>
<th>Antal</th>
<th>Beløb</th>
<th>Modkonto</th>
<th>Formål</th>
<th>Kunde/Ordre</th>
<th>Status</th>
<th>Handlinger</th>
</tr>
</thead>
<tbody id="linesTable">
<tr>
<td colspan="10" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Pending Files Tab -->
<div class="tab-pane fade" id="pending-files-content">
<!-- Pending Files Table -->
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">⏳ Filer der mangler behandling</h5>
<button class="btn btn-sm btn-outline-primary" onclick="loadPendingFiles()">
<i class="bi bi-arrow-clockwise me-2"></i>Opdater
</button>
</div>
<!-- Bulk Actions Bar -->
<div class="alert alert-light border mb-3" id="bulkActionsBar" style="display: none;">
<div class="d-flex align-items-center justify-content-between">
<div>
<strong><span id="selectedFilesCount">0</span> filer valgt</strong>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-success" onclick="bulkCreateInvoices()" title="Opret fakturaer">
<i class="bi bi-plus-circle me-1"></i>Opret Fakturaer
</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="bulkReprocess()" title="Genbehandle filer">
<i class="bi bi-arrow-clockwise me-1"></i>Genbehandle
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="bulkDelete()" title="Slet filer">
<i class="bi bi-trash me-1"></i>Slet
</button>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" class="form-check-input" id="selectAllFiles" onchange="toggleSelectAll()">
</th>
<th>Filnavn</th>
<th>Upload Dato</th>
<th>Status</th>
<th>Quick Analysis</th>
<th>Leverandør</th>
<th>Template</th>
<th>Handlinger</th>
</tr>
</thead>
<tbody id="pendingFilesTable">
<tr>
<td colspan="8" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create/Edit Invoice Modal -->
<div class="modal fade" id="invoiceModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ny Leverandørfaktura</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="invoiceForm">
<input type="hidden" id="invoiceId">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Fakturanummer *</label>
<input type="text" class="form-control" id="invoiceNumber" required>
</div>
<div class="col-md-6">
<label class="form-label">Leverandør *</label>
<select class="form-select" id="vendorId" required>
<option value="">Vælg leverandør...</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Fakturadato *</label>
<input type="date" class="form-control" id="invoiceDate" required>
</div>
<div class="col-md-6">
<label class="form-label">Forfaldsdato</label>
<input type="date" class="form-control" id="dueDate">
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">Total beløb (inkl. moms) *</label>
<input type="number" step="0.01" class="form-control" id="totalAmount" required>
</div>
<div class="col-md-4">
<label class="form-label">Moms beløb</label>
<input type="number" step="0.01" class="form-control" id="vatAmount">
</div>
<div class="col-md-4">
<label class="form-label">Valuta</label>
<select class="form-select" id="currency">
<option value="DKK" selected>DKK</option>
<option value="EUR">EUR</option>
<option value="USD">USD</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Beskrivelse</label>
<textarea class="form-control" id="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Noter (interne)</label>
<textarea class="form-control" id="notes" rows="2"></textarea>
</div>
<!-- Line Items -->
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Linjer</label>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addLine()">
<i class="bi bi-plus-circle me-1"></i>Tilføj linje
</button>
</div>
<div id="lineItems">
<!-- Lines will be added dynamically -->
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" onclick="saveInvoice()">Gem</button>
</div>
</div>
</div>
</div>
<!-- View Invoice Details Modal -->
<div class="modal fade" id="viewInvoiceModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title"><i class="bi bi-receipt me-2"></i>Fakturadetaljer</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="invoiceDetails" style="max-height: 75vh; overflow-y: auto;">
<!-- Details will be loaded dynamically -->
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
<button type="button" class="btn btn-success" id="approveBtn" onclick="approveInvoice()">
<i class="bi bi-check-circle me-1"></i>Godkend
</button>
<button type="button" class="btn btn-primary" id="sendToEconomicBtn" onclick="sendToEconomic()">
<i class="bi bi-send me-1"></i>Send til e-conomic
</button>
</div>
</div>
</div>
</div>
<!-- Upload Invoice Modal -->
<div class="modal fade" id="uploadModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-cloud-upload me-2"></i>Upload Leverandørfaktura</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
<strong>AI-Analyse:</strong> Systemet vil automatisk udtrække fakturadata (CVR, beløb, linjer) fra PDF/billede via AI.
Leverandør matches automatisk via CVR nummer.
</div>
<form id="uploadForm" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Vælg filer (PDF, PNG, JPG) *</label>
<input type="file" class="form-control" id="fileInput" accept=".pdf,.png,.jpg,.jpeg" multiple required>
<div class="form-text">Max 50 MB pr. fil. Vælg flere filer med Cmd/Ctrl. AI vil udtrække CVR, fakturanummer, beløb og linjer.</div>
<div id="fileCount" class="mt-2 text-muted small"></div>
</div>
<!-- Upload Progress -->
<div id="uploadProgress" class="d-none mb-3">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%" id="progressBar"></div>
</div>
<div class="text-center mt-2" id="progressText">Uploader...</div>
</div>
<!-- Upload Result -->
<div id="uploadResult" class="d-none">
<!-- Will show success/error/duplicate alerts -->
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-success" onclick="uploadInvoice()" id="uploadBtn">
<i class="bi bi-cloud-upload me-2"></i>Upload & Analyser
</button>
</div>
</div>
</div>
</div>
<!-- Review Extracted Data Modal -->
<div class="modal fade" id="reviewExtractedDataModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-search me-2"></i>Gennemse Udtrukne Data</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="reviewModalContent">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</div>
</div>
<div class="modal-footer">
<input type="hidden" id="reviewModalFileId">
<button type="button" class="btn btn-warning" onclick="reprocessFromModal()">
<i class="bi bi-arrow-clockwise me-2"></i>Genbehandle
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
<button type="button" class="btn btn-primary" onclick="openManualEntryMode()">
<i class="bi bi-pencil me-2"></i>Manuel Indtastning
</button>
<button type="button" class="btn btn-success" onclick="createInvoiceFromExtraction()">
<i class="bi bi-check-circle me-2"></i>Opret Faktura
</button>
</div>
</div>
</div>
</div>
<!-- Manual Entry Modal -->
<div class="modal fade" id="manualEntryModal" tabindex="-1">
<div class="modal-dialog" style="max-width: 95%; width: 1800px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-pencil-square me-2"></i>Manuel Indtastning af Faktura</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="manualEntryFileId">
<div class="row">
<!-- Left: PDF Text -->
<div class="col-md-4">
<h6 class="mb-3">
PDF Dokument
<button type="button" class="btn btn-sm btn-outline-secondary float-end" onclick="togglePdfView()">
<i class="bi bi-file-earmark-text" id="pdfViewIcon"></i> Vis Original
</button>
</h6>
<!-- PDF Text View (default, selectable) -->
<div id="pdfTextView" class="bg-body border rounded p-3" style="height: 700px; overflow: auto; font-family: monospace; font-size: 0.85rem; line-height: 1.4; user-select: text;">
<div class="text-muted text-center py-5">Indlæser PDF tekst...</div>
</div>
<!-- PDF Original View (hidden by default) -->
<iframe id="manualEntryPdfViewer" type="application/pdf" width="100%" height="700px" style="border: 1px solid #ddd; border-radius: 4px; display: none;"></iframe>
<div class="alert alert-info mt-2 py-2 px-3" style="font-size: 0.85rem;">
💡 <strong>Tip:</strong> Markér tekst og klik på et felt for at indsætte
</div>
</div>
<!-- Right: Form -->
<div class="col-md-8">
<h6 class="mb-3">Faktura Detaljer</h6>
<form id="manualEntryForm">
<!-- Vendor Selection -->
<div class="mb-3">
<label class="form-label">Leverandør *</label>
<select class="form-select" id="manualVendorId" required>
<option value="">Vælg leverandør...</option>
</select>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="openCreateVendorInline()">
<i class="bi bi-plus-circle me-1"></i>Opret ny leverandør
</button>
</div>
<!-- Invoice Details -->
<div class="row mb-3">
<div class="col-md-8">
<label class="form-label fw-bold">Fakturanummer *</label>
<input type="text" class="form-control form-control-lg manual-entry-field" id="manualInvoiceNumber" required>
</div>
<div class="col-md-4">
<label class="form-label fw-bold">Type</label>
<select class="form-select form-select-lg" id="manualInvoiceType">
<option value="invoice">Faktura</option>
<option value="credit_note">Kreditnota</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label fw-bold">Fakturadato *</label>
<input type="date" class="form-control form-control-lg manual-entry-field" id="manualInvoiceDate" required>
</div>
<div class="col-md-6">
<label class="form-label fw-bold">Forfaldsdato</label>
<input type="date" class="form-control form-control-lg manual-entry-field" id="manualDueDate">
</div>
</div>
<div class="row mb-3">
<div class="col-md-8">
<label class="form-label fw-bold">Total Beløb *</label>
<input type="number" step="0.01" class="form-control form-control-lg manual-entry-field" id="manualTotalAmount" required>
</div>
<div class="col-md-4">
<label class="form-label fw-bold">Valuta</label>
<select class="form-select form-select-lg" id="manualCurrency">
<option value="DKK">DKK</option>
<option value="EUR">EUR</option>
<option value="USD">USD</option>
</select>
</div>
</div>
<!-- Line Items -->
<div class="mb-3">
<label class="form-label">Produktlinier</label>
<div class="alert alert-light border py-2 px-3 mb-2" style="font-size: 0.875rem;">
💡 <strong>Momskoder:</strong>
<span class="badge bg-success ms-2">I25</span> 25% moms (standard) ·
<span class="badge bg-warning text-dark">I52</span> Omvendt betalingspligt ·
<span class="badge bg-secondary">I0</span> 0% moms (momsfri)
</div>
<div id="manualLineItems">
<!-- Lines will be added here -->
</div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addManualLine()">
<i class="bi bi-plus-circle me-1"></i>Tilføj linje
</button>
</div>
<div class="mb-3">
<label class="form-label">Noter</label>
<textarea class="form-control" id="manualNotes" rows="2"></textarea>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-success" onclick="saveManualInvoice()">
<i class="bi bi-save me-2"></i>Gem Faktura
</button>
</div>
</div>
</div>
</div>
<!-- =============================================
QUICK OPRET LEVERANDØR — Split-view modal
Venstre: PDF iframe | Højre: Vendor form
============================================== -->
<div class="modal fade" id="quickVendorSplitModal" tabindex="-1" style="--bs-modal-width: 100%;">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header py-2 border-bottom">
<h5 class="modal-title"><i class="bi bi-person-plus me-2"></i>Opret / Link Leverandør</h5>
<div class="ms-3 d-flex align-items-center gap-2">
<span class="badge bg-secondary" id="qvSplitFilename" style="font-size:.85rem"></span>
</div>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0 d-flex" style="height: calc(100vh - 120px); overflow:hidden;">
<!-- LEFT: PDF viewer -->
<div class="d-flex flex-column border-end" style="width:58%; min-width:400px; height:100%;">
<div class="px-3 py-2 bg-body-tertiary border-bottom small text-muted flex-shrink-0">
<i class="bi bi-file-pdf text-danger me-1"></i>Faktura PDF
</div>
<iframe id="qvPdfFrame" src="" style="flex:1 1 0; min-height:0; border:none; width:100%;" title="PDF Preview"></iframe>
</div>
<!-- RIGHT: Vendor form -->
<div class="d-flex flex-column" style="width:42%; overflow-y:auto;">
<div class="px-4 py-3 bg-body-tertiary border-bottom">
<span class="small text-muted">Udfyld leverandøroplysninger — felter er preudfyldt fra faktura-PDF</span>
</div>
<div class="px-4 py-3">
<input type="hidden" id="qvFileId">
<input type="hidden" id="qvExistingVendorId">
<!-- Search existing -->
<div class="card mb-3 border-primary">
<div class="card-header py-2 bg-primary text-white small"><i class="bi bi-search me-1"></i>Link eksisterende leverandør</div>
<div class="card-body py-2">
<div class="input-group input-group-sm">
<input type="text" class="form-control" id="qvSearchInput" placeholder="Søg navn eller CVR..." oninput="qvSearchVendors(this.value)">
<button class="btn btn-outline-secondary" type="button" onclick="qvSearchVendors(document.getElementById('qvSearchInput').value)"><i class="bi bi-search"></i></button>
</div>
<div id="qvSearchResults" class="list-group mt-2" style="max-height:160px; overflow-y:auto;">
<div class="list-group-item text-muted small py-1">Søg for at finde eksisterende leverandør</div>
</div>
</div>
</div>
<div class="text-center text-muted my-2 small">— eller opret ny leverandør nedenfor —</div>
<form id="qvVendorForm" autocomplete="off">
<div class="row g-2 mb-2">
<div class="col-8">
<label class="form-label small mb-1">Navn *</label>
<input type="text" class="form-control form-control-sm" id="qvName" required placeholder="Firma navn">
</div>
<div class="col-4">
<label class="form-label small mb-1">CVR</label>
<input type="text" class="form-control form-control-sm" id="qvCVR" maxlength="8" placeholder="12345678">
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-6">
<label class="form-label small mb-1">Email</label>
<input type="email" class="form-control form-control-sm" id="qvEmail" placeholder="kontakt@firma.dk">
</div>
<div class="col-6">
<label class="form-label small mb-1">Telefon</label>
<input type="tel" class="form-control form-control-sm" id="qvPhone" placeholder="+45 12 34 56 78">
</div>
</div>
<div class="mb-2">
<label class="form-label small mb-1">Adresse</label>
<input type="text" class="form-control form-control-sm" id="qvAddress" placeholder="Vejnavn nr.">
</div>
<div class="row g-2 mb-2">
<div class="col-4">
<label class="form-label small mb-1">Postnr.</label>
<input type="text" class="form-control form-control-sm" id="qvPostal" maxlength="10">
</div>
<div class="col-8">
<label class="form-label small mb-1">By</label>
<input type="text" class="form-control form-control-sm" id="qvCity">
</div>
</div>
<div class="mb-2">
<label class="form-label small mb-1">Website / domæne</label>
<input type="text" class="form-control form-control-sm" id="qvDomain" placeholder="firma.dk">
</div>
<div class="mb-2">
<label class="form-label small mb-1">Kategori</label>
<select class="form-select form-select-sm" id="qvCategory">
<option value="general">Generel</option>
<option value="telecom">Telecom</option>
<option value="hardware">Hardware</option>
<option value="software">Software</option>
<option value="services">Services</option>
<option value="payroll">Løn / HR</option>
<option value="utilities">Forsyning</option>
<option value="insurance">Forsikring</option>
<option value="rent">Husleje / Lokaler</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small mb-1">Noter (inkl. bank/IBAN info)</label>
<textarea class="form-control form-control-sm" id="qvNotes" rows="3" placeholder="IBAN, kontonummer, BIC/SWIFT, betalingsbetingelser..."></textarea>
</div>
</form>
<!-- Status alert -->
<div id="qvStatusAlert" class="alert d-none py-2 small"></div>
</div>
</div>
</div><!-- /.modal-body -->
<div class="modal-footer py-2 border-top justify-content-between">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Luk</button>
<div class="d-flex gap-2">
<button type="button" class="btn btn-success btn-sm" onclick="saveQuickVendor()">
<i class="bi bi-person-plus me-1"></i>Opret og link leverandør
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Link/Create Vendor Modal -->
<div class="modal fade" id="linkVendorModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Link eller Opret Leverandør</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="linkVendorFileId">
<input type="hidden" id="linkVendorName">
<input type="hidden" id="linkVendorCVR">
<div class="alert alert-info">
<strong>Fundet leverandør:</strong><br>
Navn: <span id="linkVendorDisplayName"></span><br>
CVR: <span id="linkVendorDisplayCVR"></span>
</div>
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#linkExisting">Link Eksisterende</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#createNew">Opret Ny</button>
</li>
</ul>
<div class="tab-content">
<!-- Link Existing -->
<div class="tab-pane fade show active" id="linkExisting">
<div class="mb-3">
<label class="form-label">Søg efter leverandør</label>
<input type="text" class="form-control" id="vendorSearchInput" placeholder="Søg navn eller CVR...">
</div>
<div id="vendorSearchResults" class="list-group" style="max-height: 300px; overflow-y: auto;">
<div class="text-center text-muted py-3">Søg for at finde leverandører</div>
</div>
</div>
<!-- Create New -->
<div class="tab-pane fade" id="createNew">
<form id="createVendorForm">
<div class="mb-3">
<label class="form-label">Navn *</label>
<input type="text" class="form-control" id="newVendorName" required>
</div>
<div class="mb-3">
<label class="form-label">CVR Nummer</label>
<input type="text" class="form-control" id="newVendorCVR" maxlength="8">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="newVendorEmail">
</div>
<div class="mb-3">
<label class="form-label">Telefon</label>
<input type="tel" class="form-control" id="newVendorPhone">
</div>
<div class="mb-3">
<label class="form-label">Adresse</label>
<textarea class="form-control" id="newVendorAddress" rows="2"></textarea>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" onclick="saveVendorLink()" id="linkExistingBtn">
<i class="bi bi-link-45deg me-2"></i>Link Leverandør
</button>
<button type="button" class="btn btn-success" onclick="createAndLinkVendor()" id="createNewBtn" style="display: none;">
<i class="bi bi-plus-circle me-2"></i>Opret og Link
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Global state
let currentInvoiceId = null;
let currentFilter = 'all';
let allInvoices = [];
let lastSelectedText = '';
let lastFocusedField = null;
// Load data on page load
document.addEventListener('DOMContentLoaded', () => {
loadStats();
loadUnhandledFiles(); // Load unhandled files by default (first tab)
loadVendors();
setupManualEntryTextSelection();
setDefaultDates();
checkEmailContext(); // Check if coming from email
});
// Check if coming from email context
async function checkEmailContext() {
const emailContext = sessionStorage.getItem('supplierInvoiceContext');
if (emailContext) {
try {
const context = JSON.parse(emailContext);
console.log('📧 Processing email context:', context);
// Show notification
console.log('📧 Behandler faktura fra email:', context.subject);
// Process attachments if any
if (context.attachments && context.attachments.length > 0) {
console.log(`📎 Found ${context.attachments.length} attachments`);
// Find PDF attachments
const pdfAttachments = context.attachments.filter(att =>
att.filename && att.filename.toLowerCase().endsWith('.pdf')
);
if (pdfAttachments.length > 0) {
console.log(`📄 Processing ${pdfAttachments.length} PDF attachments`);
for (const attachment of pdfAttachments) {
try {
// Download attachment and upload to supplier invoices
console.log(`⬇️ Downloading attachment: ${attachment.filename}`);
const attachmentResponse = await fetch(`/api/v1/emails/${context.emailId}/attachments/${attachment.id}`);
if (!attachmentResponse.ok) {
console.error(`Failed to download attachment ${attachment.id}`);
continue;
}
const blob = await attachmentResponse.blob();
const file = new File([blob], attachment.filename, { type: 'application/pdf' });
// Upload to supplier invoices
console.log(`⬆️ Uploading to supplier invoices: ${attachment.filename}`);
const formData = new FormData();
formData.append('file', file);
const uploadResponse = await fetch('/api/v1/supplier-invoices/upload', {
method: 'POST',
body: formData
});
if (uploadResponse.ok) {
const result = await uploadResponse.json();
console.log('✅ Upload successful:', result);
alert(`✅ Faktura ${attachment.filename} uploadet og behandlet`);
} else {
const errorData = await uploadResponse.json();
console.error('Upload failed:', errorData);
alert(`❌ Kunne ikke uploade ${attachment.filename}: ${errorData.detail || 'Ukendt fejl'}`);
}
} catch (error) {
console.error(`Error processing attachment ${attachment.filename}:`, error);
alert(`❌ Fejl ved behandling af ${attachment.filename}`);
}
}
// Reload pending files list after uploads
setTimeout(() => {
loadPendingFiles();
}, 1000);
} else {
alert('⚠️ Ingen PDF vedhæftninger fundet i emailen');
}
} else {
alert('⚠️ Emailen har ingen vedhæftninger');
}
// Clear context after use
sessionStorage.removeItem('supplierInvoiceContext');
} catch (error) {
console.error('Failed to process email context:', error);
alert('❌ Kunne ikke behandle email vedhæftninger');
sessionStorage.removeItem('supplierInvoiceContext');
}
}
}
// Set default dates
function setDefaultDates() {
const today = new Date().toISOString().split('T')[0];
const dueDateObj = new Date();
dueDateObj.setDate(dueDateObj.getDate() + 30);
const dueDate = dueDateObj.toISOString().split('T')[0];
document.getElementById('invoiceDate').value = today;
document.getElementById('dueDate').value = dueDate;
}
// Load statistics
async function loadStats() {
try {
const response = await fetch('/api/v1/supplier-invoices/stats/overview');
const stats = await response.json();
document.getElementById('statOverdueCount').textContent = stats.overdue_count || 0;
document.getElementById('statOverdueAmount').textContent = formatCurrency(stats.overdue_amount);
document.getElementById('statDueSoonCount').textContent = stats.due_soon_count || 0;
document.getElementById('statDueSoonAmount').textContent = formatCurrency(stats.unpaid_amount);
document.getElementById('statPendingCount').textContent = stats.pending_count || 0;
document.getElementById('statPendingAmount').textContent = formatCurrency(stats.unpaid_amount);
document.getElementById('statTotalCount').textContent = stats.total_invoices || 0;
document.getElementById('statUnpaidAmount').textContent = formatCurrency(stats.unpaid_amount);
} catch (error) {
console.error('Failed to load stats:', error);
}
}
// Load invoices
async function loadInvoices(status = null) {
try {
let url = '/api/v1/supplier-invoices';
const params = new URLSearchParams();
if (status && status !== 'all') {
if (status === 'overdue') {
params.append('overdue_only', 'true');
} else {
params.append('status', status);
}
}
if (params.toString()) {
url += '?' + params.toString();
}
const response = await fetch(url);
allInvoices = await response.json();
renderInvoices(allInvoices);
} catch (error) {
console.error('Failed to load invoices:', error);
document.getElementById('invoicesTable').innerHTML = `
<tr>
<td colspan="8" class="text-center text-danger py-4">
<i class="bi bi-exclamation-triangle me-2"></i>
Fejl ved indlæsning af fakturaer
</td>
</tr>
`;
}
}
// Render invoices table
function renderInvoices(invoices) {
const tbody = document.getElementById('invoicesTable');
if (!invoices || invoices.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-muted py-4">
<i class="bi bi-inbox me-2"></i>
Ingen fakturaer fundet
</td>
</tr>
`;
return;
}
tbody.innerHTML = invoices.map(inv => {
const isCreditNote = inv.invoice_type === 'credit_note';
const isLocked = inv.economic_voucher_number; // Locked if already sent to e-conomic
return `
<tr class="${isCreditNote ? 'table-warning' : ''}">
<td onclick="event.stopPropagation();">
<input type="checkbox" class="form-check-input invoice-checkbox"
data-invoice-id="${inv.id}"
${isLocked ? 'disabled' : ''}
onchange="updateInvoiceBulkActionsBar()">
</td>
<td onclick="viewInvoice(${inv.id})" style="cursor: pointer;">
${isCreditNote ? '<i class="bi bi-arrow-return-left text-warning me-1" title="Kreditnota"></i>' : ''}
<strong>${inv.invoice_number}</strong>
</td>
<td onclick="viewInvoice(${inv.id})" style="cursor: pointer;">${inv.vendor_full_name || inv.vendor_name || '-'}</td>
<td onclick="viewInvoice(${inv.id})" style="cursor: pointer;">${formatDate(inv.invoice_date)}</td>
<td onclick="viewInvoice(${inv.id})" style="cursor: pointer;">${formatDate(inv.due_date)}</td>
<td onclick="viewInvoice(${inv.id})" style="cursor: pointer;"><strong class="${isCreditNote ? 'text-danger' : ''}">${formatCurrency(inv.total_amount)}</strong></td>
<td onclick="viewInvoice(${inv.id})" style="cursor: pointer;">${getStatusBadge(inv.computed_status || inv.status)}</td>
<td onclick="viewInvoice(${inv.id})" style="cursor: pointer;">${inv.economic_voucher_number ? `<span class="badge bg-success">Bilag #${inv.economic_voucher_number}</span>` : '-'}</td>
<td onclick="event.stopPropagation();">
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="viewInvoice(${inv.id})" title="Vis detaljer">
<i class="bi bi-eye"></i>
</button>
${inv.status === 'pending' && !isCreditNote ? `
<button class="btn btn-outline-success" onclick="quickApprove(${inv.id})" title="Godkend">
<i class="bi bi-check-circle"></i>
</button>
` : ''}
${inv.status === 'approved' && !inv.economic_voucher_number ? `
<button class="btn btn-outline-primary" onclick="quickSendToEconomic(${inv.id})" title="Send til e-conomic">
<i class="bi bi-send"></i>
</button>
` : ''}
${!inv.economic_voucher_number ? `
<button class="btn btn-outline-success" onclick="markInvoiceAsPaid(${inv.id})" title="Marker som betalt">
<i class="bi bi-cash-coin"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteInvoice(${inv.id})" title="Slet">
<i class="bi bi-trash"></i>
</button>
` : ''}
</div>
</td>
</tr>
`}).join('');
// Reset bulk selection
updateInvoiceBulkActionsBar();
}
// Load vendors for dropdown
async function loadVendors() {
try {
const response = await fetch('/api/v1/vendors');
const vendors = await response.json();
const select = document.getElementById('vendorId');
select.innerHTML = '<option value="">Vælg leverandør...</option>' +
vendors.map(v => `<option value="${v.id}">${v.name}</option>`).join('');
} catch (error) {
console.error('Failed to load vendors:', error);
}
}
// Helper function for status badge class
function getStatusBadgeClass(status) {
const classes = {
'pending': 'bg-warning text-dark',
'approved': 'bg-info',
'sent_to_economic': 'bg-success',
'paid': 'bg-success',
'overdue': 'bg-danger',
'cancelled': 'bg-secondary',
'unpaid': 'bg-warning text-dark'
};
return classes[status] || 'bg-secondary';
}
// Helper function for status label
function getStatusLabel(status) {
const labels = {
'pending': 'Afventer',
'approved': 'Godkendt',
'sent_to_economic': 'Sendt',
'paid': 'Betalt',
'overdue': 'Overskredet',
'cancelled': 'Annulleret',
'unpaid': 'Ubetalt'
};
return labels[status] || status;
}
// Helper function to view invoice details
async function viewInvoiceDetails(invoiceId) {
// Reuse existing viewInvoice function
await viewInvoice(invoiceId);
}
// Helper function to mark single invoice as paid
async function markSingleAsPaid(invoiceId) {
if (!confirm('Marker denne faktura som betalt?')) return;
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/mark-paid`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
paid_date: new Date().toISOString().split('T')[0]
})
});
if (response.ok) {
alert('✅ Faktura markeret som betalt');
await loadPaymentView();
} else {
throw new Error('Failed to mark as paid');
}
} catch (error) {
console.error('Failed to mark as paid:', error);
alert('❌ Fejl ved markering som betalt');
}
}
// Helper function to send single invoice to e-conomic
async function sendToEconomicById(invoiceId) {
if (!confirm('Send denne faktura til e-conomic?')) return;
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/send-to-economic`, {
method: 'POST'
});
if (response.ok) {
alert('✅ Faktura sendt til e-conomic');
await loadReadyForBookingView();
} else {
const error = await response.json();
throw new Error(error.detail || 'Failed to send');
}
} catch (error) {
console.error('Failed to send to e-conomic:', error);
alert('❌ Fejl ved afsendelse: ' + error.message);
}
}
// Open existing invoice for full editing (like manual entry)
async function editInvoiceFull(invoiceId) {
try {
// Fetch invoice with lines
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}`);
const invoice = await response.json();
// Load vendors
await loadVendorsForManual();
// Clear and populate form
document.getElementById('manualEntryForm').reset();
document.getElementById('manualLineItems').innerHTML = '';
document.getElementById('manualEntryFileId').value = '';
// Set invoice data
document.getElementById('manualVendorId').value = invoice.vendor_id || '';
document.getElementById('manualInvoiceNumber').value = invoice.invoice_number || '';
document.getElementById('manualInvoiceType').value = invoice.invoice_type || 'invoice';
document.getElementById('manualInvoiceDate').value = invoice.invoice_date || '';
document.getElementById('manualDueDate').value = invoice.due_date || '';
document.getElementById('manualTotalAmount').value = invoice.total_amount || '';
document.getElementById('manualCurrency').value = invoice.currency || 'DKK';
document.getElementById('manualNotes').value = invoice.notes || '';
// Store invoice ID for update instead of create
document.getElementById('manualEntryForm').dataset.editingInvoiceId = invoiceId;
// Add lines
const lines = invoice.lines || [];
if (lines.length > 0) {
lines.forEach(line => {
addManualLine();
const lineNum = manualLineCounter;
document.getElementById(`manualLineDesc${lineNum}`).value = line.description || '';
document.getElementById(`manualLineQty${lineNum}`).value = line.quantity || 1;
document.getElementById(`manualLinePrice${lineNum}`).value = Math.abs(line.unit_price || 0);
// Set VAT code
const vatCodeSelect = document.getElementById(`manualLineVatCode${lineNum}`);
if (line.vat_code) {
vatCodeSelect.value = line.vat_code;
} else if (line.vat_rate === 25) {
vatCodeSelect.value = 'I25';
} else if (line.vat_rate === 0) {
vatCodeSelect.value = 'I0';
}
document.getElementById(`manualLineContra${lineNum}`).value = line.contra_account || '';
});
} else {
addManualLine();
}
// Try to load PDF if file_id exists in notes
const pdfTextView = document.getElementById('pdfTextView');
const pdfViewer = document.getElementById('manualEntryPdfViewer');
if (invoice.notes) {
// Try multiple patterns: "file_id: 4" or "fil ID 13" or "file ID 13"
let match = invoice.notes.match(/file_id:\s*(\d+)/i);
if (!match) {
match = invoice.notes.match(/fil\s+ID\s+(\d+)/i);
}
if (!match) {
match = invoice.notes.match(/file\s+ID\s+(\d+)/i);
}
if (match) {
const fileId = match[1];
console.log('📄 Found file_id:', fileId);
try {
// Load PDF text
const textResponse = await fetch(`/api/v1/supplier-invoices/files/${fileId}/pdf-text`);
if (textResponse.ok) {
const textData = await textResponse.json();
pdfTextView.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word;">${textData.pdf_text || 'Ingen tekst fundet'}</pre>`;
console.log('✅ PDF text loaded');
} else {
console.error('Failed to load PDF text:', textResponse.status);
pdfTextView.innerHTML = '<div class="text-muted text-center py-5">Kunne ikke indlæse PDF tekst</div>';
}
// Set PDF viewer URL (will be loaded when user clicks "Vis Original")
pdfViewer.src = `/api/v1/supplier-invoices/files/${fileId}/pdf`;
console.log('✅ PDF viewer URL set');
} catch (error) {
console.error('Failed to load PDF:', error);
pdfTextView.innerHTML = '<div class="text-muted text-center py-5">Fejl ved indlæsning af PDF</div>';
}
} else {
console.warn('Could not parse file_id from notes');
pdfTextView.innerHTML = '<div class="text-muted text-center py-5">Ingen PDF tilknyttet denne faktura</div>';
}
} else {
console.log('No file_id in notes');
pdfTextView.innerHTML = '<div class="text-muted text-center py-5">Ingen PDF tilknyttet denne faktura</div>';
pdfViewer.style.display = 'none';
}
// Change modal title
document.querySelector('#manualEntryModal .modal-title').innerHTML = '<i class="bi bi-pencil-square me-2"></i>Rediger Faktura';
// Open modal
const modal = new bootstrap.Modal(document.getElementById('manualEntryModal'));
modal.show();
} catch (error) {
console.error('Failed to open invoice for editing:', error);
alert('Fejl ved åbning af faktura: ' + error.message);
}
}
// ========== PENDING FILES FUNCTIONS ==========
// Load pending files count for badge (on page load)
async function loadPendingFilesCount() {
try {
const response = await fetch('/api/v1/pending-supplier-invoice-files');
const data = await response.json();
const count = data.count || 0;
const badge = document.getElementById('pendingFilesCount');
if (count > 0) {
badge.textContent = count;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
} catch (error) {
console.error('Failed to load pending files count:', error);
}
}
// ========== PAYMENT VIEW FUNCTIONS ==========
async function loadPaymentView() {
try {
const tbody = document.getElementById('paymentTable');
tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></td></tr>`;
const response = await fetch('/api/v1/supplier-invoices?approved_only=true&unpaid_only=true');
const invoices = await response.json();
// Sort by due date (earliest first)
invoices.sort((a, b) => {
if (!a.due_date) return 1;
if (!b.due_date) return -1;
return new Date(a.due_date) - new Date(b.due_date);
});
if (invoices.length === 0) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">Ingen fakturaer til betaling</td></tr>`;
return;
}
let html = '';
const today = new Date();
invoices.forEach(invoice => {
const dueDate = invoice.due_date ? new Date(invoice.due_date) : null;
const isOverdue = dueDate && dueDate < today;
const isDueSoon = dueDate && !isOverdue && ((dueDate - today) / (1000 * 60 * 60 * 24)) <= 7;
let rowClass = '';
if (isOverdue) rowClass = 'table-danger';
else if (isDueSoon) rowClass = 'table-warning';
html += `
<tr class="${rowClass}">
<td><input type="checkbox" class="form-check-input payment-checkbox" data-invoice-id="${invoice.id}" data-amount="${invoice.total_amount}" onchange="updatePaymentSelection()"></td>
<td>${invoice.due_date || '-'} ${isOverdue ? '<span class="badge bg-danger">OVERSKREDET</span>' : ''} ${isDueSoon ? '<span class="badge bg-warning text-dark">7 DAGE</span>' : ''}</td>
<td>${invoice.invoice_number || '-'}</td>
<td>${invoice.vendor_full_name || invoice.vendor_name || '-'}</td>
<td>${parseFloat(invoice.total_amount || 0).toLocaleString('da-DK')} ${invoice.currency || 'DKK'}</td>
<td><span class="badge ${getStatusBadgeClass(invoice.status)}">${getStatusLabel(invoice.status)}</span></td>
<td>
<button class="btn btn-sm btn-outline-secondary" onclick="editInvoiceFull(${invoice.id})" title="Fuld redigering">
<i class="bi bi-pencil-fill"></i>
</button>
<button class="btn btn-sm btn-outline-primary" onclick="viewInvoiceDetails(${invoice.id})" title="Se/Rediger detaljer">
<i class="bi bi-pencil-square"></i>
</button>
<button class="btn btn-sm btn-outline-success" onclick="markSingleAsPaid(${invoice.id})" title="Marker betalt">
<i class="bi bi-check-circle"></i>
</button>
</td>
</tr>
`;
});
tbody.innerHTML = html;
} catch (error) {
console.error('Failed to load payment view:', error);
document.getElementById('paymentTable').innerHTML = `<tr><td colspan="7" class="text-center text-danger py-4">Fejl ved indlæsning</td></tr>`;
}
}
function updatePaymentSelection() {
const checkboxes = document.querySelectorAll('.payment-checkbox:checked');
const count = checkboxes.length;
const total = Array.from(checkboxes).reduce((sum, cb) => sum + parseFloat(cb.dataset.amount || 0), 0);
document.getElementById('selectedPaymentCount').textContent = count;
document.getElementById('selectedPaymentTotal').textContent = total.toLocaleString('da-DK', {minimumFractionDigits: 2, maximumFractionDigits: 2});
document.getElementById('paymentBulkActionsBar').style.display = count > 0 ? 'block' : 'none';
}
function toggleSelectAllPayment() {
const mainCheckbox = document.getElementById('selectAllPayment');
const checkboxes = document.querySelectorAll('.payment-checkbox');
checkboxes.forEach(cb => cb.checked = mainCheckbox.checked);
updatePaymentSelection();
}
// ========== READY FOR BOOKING VIEW FUNCTIONS ==========
async function loadReadyForBookingView() {
try {
const tbody = document.getElementById('readyTable');
tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></td></tr>`;
const response = await fetch('/api/v1/supplier-invoices?approved_only=true');
const invoices = await response.json();
// Filter invoices where all lines have contra_account (ready for booking)
const readyInvoices = [];
for (const invoice of invoices) {
const lines = invoice.lines || [];
// Check if all lines have contra_account
const allLinesHaveAccount = lines.length > 0 && lines.every(line => line.contra_account);
if (allLinesHaveAccount && invoice.status !== 'sent_to_economic') {
readyInvoices.push({...invoice, line_count: lines.length});
}
}
// Update count badge
const countBadge = document.getElementById('readyCount');
if (readyInvoices.length > 0) {
countBadge.textContent = readyInvoices.length;
countBadge.style.display = 'inline-block';
} else {
countBadge.style.display = 'none';
}
if (readyInvoices.length === 0) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">Ingen fakturaer klar til bogføring</td></tr>`;
return;
}
let html = '';
readyInvoices.forEach(invoice => {
html += `
<tr>
<td><input type="checkbox" class="form-check-input ready-checkbox" data-invoice-id="${invoice.id}" onchange="updateReadySelection()"></td>
<td>${invoice.invoice_number || '-'}</td>
<td>${invoice.vendor_full_name || invoice.vendor_name || '-'}</td>
<td>${invoice.invoice_date || '-'}</td>
<td>${parseFloat(invoice.total_amount || 0).toLocaleString('da-DK')} ${invoice.currency || 'DKK'}</td>
<td><span class="badge bg-success">${invoice.line_count} linjer OK</span></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewInvoiceDetails(${invoice.id})" title="Se/Rediger detaljer">
<i class="bi bi-pencil-square"></i>
</button>
<button class="btn btn-sm btn-primary" onclick="sendToEconomicById(${invoice.id})" title="Send til e-conomic">
<i class="bi bi-send"></i>
</button>
</td>
</tr>
`;
});
tbody.innerHTML = html;
} catch (error) {
console.error('Failed to load ready view:', error);
document.getElementById('readyTable').innerHTML = `<tr><td colspan="7" class="text-center text-danger py-4">Fejl ved indlæsning</td></tr>`;
}
}
function updateReadySelection() {
const checkboxes = document.querySelectorAll('.ready-checkbox:checked');
const count = checkboxes.length;
document.getElementById('selectedReadyCount').textContent = count;
document.getElementById('readyBulkActionsBar').style.display = count > 0 ? 'block' : 'none';
}
function toggleSelectAllReady() {
const mainCheckbox = document.getElementById('selectAllReady');
const checkboxes = document.querySelectorAll('.ready-checkbox');
checkboxes.forEach(cb => cb.checked = mainCheckbox.checked);
updateReadySelection();
}
// ========== LINE ITEMS VIEW FUNCTIONS ==========
async function loadLineItems() {
try {
const tbody = document.getElementById('linesTable');
tbody.innerHTML = `<tr><td colspan="10" class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></td></tr>`;
// Get filter values
const purposeFilter = document.getElementById('linePurposeFilter').value;
const statusFilter = document.getElementById('lineStatusFilter').value;
const vendorFilter = document.getElementById('lineVendorFilter').value.toLowerCase();
// Fetch all invoices with lines
const response = await fetch('/api/v1/supplier-invoices');
const invoices = await response.json();
let allLines = [];
for (const invoice of invoices) {
const lines = invoice.lines || [];
lines.forEach(line => {
allLines.push({
...line,
invoice_number: invoice.invoice_number,
vendor_name: invoice.vendor_full_name || invoice.vendor_name,
invoice_id: invoice.id
});
});
}
// Only show lines marked for resale (or no purpose set yet)
allLines = allLines.filter(line => line.line_purpose === 'resale' || !line.line_purpose);
// Apply additional filters from dropdowns
if (purposeFilter) {
allLines = allLines.filter(line => line.line_purpose === purposeFilter);
}
if (statusFilter === 'not_invoiced') {
allLines = allLines.filter(line => !line.is_invoiced_to_customer);
} else if (statusFilter === 'invoiced') {
allLines = allLines.filter(line => line.is_invoiced_to_customer);
}
if (vendorFilter) {
allLines = allLines.filter(line => (line.vendor_name || '').toLowerCase().includes(vendorFilter));
}
if (allLines.length === 0) {
tbody.innerHTML = `<tr><td colspan="10" class="text-center text-muted py-4">Ingen varelinjer fundet</td></tr>`;
return;
}
let html = '';
allLines.forEach(line => {
const purposeOptions = ['resale', 'internal', 'project', 'stock'];
const purposeLabels = {
'resale': 'Videresalg',
'internal': 'Internt',
'project': 'Projekt',
'stock': 'Lager'
};
html += `
<tr>
<td>${line.invoice_number || '-'}</td>
<td>${line.vendor_name || '-'}</td>
<td>${line.description || '-'}</td>
<td>${line.quantity || '-'}</td>
<td>${parseFloat(line.total_amount || 0).toLocaleString('da-DK')}</td>
<td>
<input type="text" class="form-control form-control-sm" style="width: 80px;"
value="${line.contra_account || ''}"
onblur="updateLineField(${line.id}, ${line.invoice_id}, 'contra_account', this.value)"
placeholder="XXXX">
</td>
<td>
<select class="form-select form-select-sm" style="width: 120px;"
onchange="updateLineField(${line.id}, ${line.invoice_id}, 'line_purpose', this.value)">
<option value="">Vælg...</option>
${purposeOptions.map(opt => `<option value="${opt}" ${line.line_purpose === opt ? 'selected' : ''}>${purposeLabels[opt]}</option>`).join('')}
</select>
</td>
<td>
<input type="text" class="form-control form-control-sm" style="width: 120px;"
value="${line.resale_order_number || ''}"
onblur="updateLineField(${line.id}, ${line.invoice_id}, 'resale_order_number', this.value)"
placeholder="Ordre/Kunde">
</td>
<td>
${line.is_invoiced_to_customer ?
`<span class="badge bg-success">Faktureret</span>` :
`<span class="badge bg-secondary">Afventer</span>`}
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewInvoiceDetails(${line.invoice_id})" title="Se/Rediger faktura">
<i class="bi bi-pencil-square"></i>
</button>
</td>
</tr>
`;
});
tbody.innerHTML = html;
} catch (error) {
console.error('Failed to load line items:', error);
document.getElementById('linesTable').innerHTML = `<tr><td colspan="10" class="text-center text-danger py-4">Fejl ved indlæsning</td></tr>`;
}
}
async function updateLineField(lineId, invoiceId, field, value) {
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/lines/${lineId}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({[field]: value})
});
if (!response.ok) throw new Error('Update failed');
console.log(`✅ Updated line ${lineId} ${field} = ${value}`);
// Reload line items to refresh view
setTimeout(() => loadLineItems(), 500);
} catch (error) {
console.error('Failed to update line field:', error);
alert('Kunne ikke opdatere felt');
}
}
function clearLineFilters() {
document.getElementById('linePurposeFilter').value = '';
document.getElementById('lineStatusFilter').value = '';
document.getElementById('lineVendorFilter').value = '';
loadLineItems();
}
// Switch to invoices tab
function switchToPaymentTab() {
// Load invoices sorted by due date for payment view
loadPaymentView();
}
function switchToReadyTab() {
// Load invoices ready for booking (all lines have contra_account)
loadReadyForBookingView();
}
function switchToLinesTab() {
// Load all line items with tracking
loadLineItems();
}
// NEW: Switch to unhandled files tab
function switchToUnhandledTab() {
loadUnhandledFiles();
}
// NEW: Switch to kassekladde tab
function switchToKassekladdeTab() {
loadKassekladdeView();
}
// Switch to pending files tab
function switchToPendingFilesTab() {
// Load pending files when switching to this tab
loadPendingFiles();
}
// NEW: Load unhandled files (pending + extraction_failed + requires_vendor_selection)
async function loadUnhandledFiles() {
try {
const tbody = document.getElementById('unhandledTable');
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
`;
const response = await fetch('/api/v1/supplier-invoices/files?status=pending,extraction_failed,requires_vendor_selection');
const data = await response.json();
// Update badge count
const count = data.count || data.files?.length || 0;
const badge = document.getElementById('unhandledCount');
if (count > 0) {
badge.textContent = count;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
renderUnhandledFiles(data.files || []);
} catch (error) {
console.error('Failed to load unhandled files:', error);
document.getElementById('unhandledTable').innerHTML = `
<tr>
<td colspan="7" class="text-center text-danger py-4">
<i class="bi bi-exclamation-triangle me-2"></i>
Fejl ved indlæsning
</td>
</tr>
`;
}
}
// NEW: Render unhandled files table
function renderUnhandledFiles(files) {
const tbody = document.getElementById('unhandledTable');
if (!files || files.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-check-circle me-2"></i>
Ingen filer venter på behandling
</td>
</tr>
`;
return;
}
// Build HTML first
let html = '';
for (const file of files) {
const statusBadge = getFileStatusBadge(file.status);
const vendorName = file.best_vendor_name || file.vendor_name || file.detected_vendor_name || '-';
const confRaw = file.vendor_match_confidence;
const confidence = confRaw !== null && confRaw !== undefined ? `${Math.round(confRaw * 100)}%` : '-';
const amount = file.total_amount ? formatCurrency(file.total_amount) : '-';
const uploadDate = file.uploaded_at ? new Date(file.uploaded_at).toLocaleDateString('da-DK') : '-';
html += `
<tr>
<td>
<i class="bi bi-file-pdf text-danger me-2"></i>
${file.filename}
</td>
<td>${uploadDate}</td>
<td>
${file.status === 'requires_vendor_selection' ?
`<select class="form-select form-select-sm" id="vendorSelect_${file.file_id}" onchange="selectVendorForFile(${file.file_id}, this.value)">
<option value="">Vælg leverandør...</option>
</select>` :
vendorName
}
</td>
<td>
${confidence === '100%' ?
`<span class="badge bg-success">${confidence}</span>` :
confidence
}
</td>
<td>${amount}</td>
<td>${statusBadge}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-success" onclick="openQuickVendorCreate(${file.file_id}, '${escapeHtml(file.filename)}')" title="Opret / Link leverandør">
<i class="bi bi-person-plus"></i>
</button>
<button class="btn btn-outline-warning" onclick="rerunSingleFile(${file.file_id})" title="Kør analyse igen">
<i class="bi bi-arrow-repeat"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteFile(${file.file_id})" title="Slet">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
`;
}
tbody.innerHTML = html;
// Populate vendor dropdowns after rendering
files.forEach(file => {
if (file.status === 'requires_vendor_selection') {
populateVendorDropdown(file.file_id);
}
});
}
// Populate vendor dropdown with all active vendors
async function populateVendorDropdown(fileId) {
try {
const response = await fetch('/api/v1/vendors?active_only=true');
const vendors = await response.json();
const select = document.getElementById(`vendorSelect_${fileId}`);
if (!select) return;
// Keep the "Vælg leverandør..." option
const options = vendors.map(v =>
`<option value="${v.id}">${v.name}${v.cvr_number ? ` (${v.cvr_number})` : ''}</option>`
).join('');
select.innerHTML = `<option value="">Vælg leverandør...</option>${options}`;
} catch (error) {
console.error('Failed to load vendors for dropdown:', error);
}
}
// NEW: Get status badge HTML
function getFileStatusBadge(status) {
const badges = {
'pending': '<span class="badge bg-warning text-dark">Afventer</span>',
'extraction_failed': '<span class="badge bg-danger">Fejlet</span>',
'requires_vendor_selection': '<span class="badge bg-info">Vælg leverandør</span>',
'analyzed': '<span class="badge bg-success">Analyseret</span>',
'processed': '<span class="badge bg-secondary">Behandlet</span>'
};
return badges[status] || `<span class="badge bg-secondary">${status}</span>`;
}
// NEW: Batch analyze all files
async function batchAnalyzeAllFiles() {
if (!confirm('Kør automatisk analyse på alle ubehandlede filer?\n\nDette kan tage flere minutter afhængigt af antal filer.\nSiden opdateres automatisk undervejs.')) {
return;
}
try {
showLoadingOverlay('Starter analyse...');
const response = await fetch('/api/v1/supplier-invoices/files/batch-analyze', {
method: 'POST'
});
if (!response.ok) throw new Error('Batch analysis failed');
const result = await response.json();
hideLoadingOverlay();
if (result.started === 0) {
alert(' Ingen filer at behandle.');
return;
}
alert(`${result.message}`);
// Auto-opdater tabellen hvert 10. sekund i 5 minutter
let refreshes = 0;
const maxRefreshes = 30;
const interval = setInterval(() => {
loadUnhandledFiles();
refreshes++;
if (refreshes >= maxRefreshes) clearInterval(interval);
}, 10000);
loadUnhandledFiles();
} catch (error) {
hideLoadingOverlay();
console.error('Batch analysis error:', error);
alert('❌ Fejl ved batch-analyse: ' + error.message);
}
}
// NEW: Retry extraction for failed file
async function retryExtraction(fileId) {
try {
showLoadingOverlay('Prøver igen...');
const response = await fetch(`/api/v1/supplier-invoices/files/${fileId}/retry`, {
method: 'POST'
});
if (!response.ok) throw new Error('Retry failed');
const result = await response.json();
hideLoadingOverlay();
alert(`${result.message}`);
loadUnhandledFiles();
} catch (error) {
hideLoadingOverlay();
console.error('Retry error:', error);
alert('❌ Fejl ved retry');
}
}
// ─── Quick Vendor Split-View ─────────────────────────────────────────────
async function openQuickVendorCreate(fileId, filename) {
// Reset
document.getElementById('qvFileId').value = fileId;
document.getElementById('qvExistingVendorId').value = '';
document.getElementById('qvSplitFilename').textContent = filename;
document.getElementById('qvName').value = '';
document.getElementById('qvCVR').value = '';
document.getElementById('qvEmail').value = '';
document.getElementById('qvPhone').value = '';
document.getElementById('qvAddress').value = '';
document.getElementById('qvPostal').value = '';
document.getElementById('qvCity').value = '';
document.getElementById('qvDomain').value = '';
document.getElementById('qvNotes').value = '';
document.getElementById('qvSearchInput').value = '';
document.getElementById('qvSearchResults').innerHTML = '<div class="list-group-item text-muted small py-1">Søg for at finde eksisterende leverandør</div>';
document.getElementById('qvStatusAlert').className = 'alert d-none py-2 small';
// Load PDF in iframe
document.getElementById('qvPdfFrame').src = `/api/v1/supplier-invoices/files/${fileId}/download`;
// Open modal immediately
const modal = new bootstrap.Modal(document.getElementById('quickVendorSplitModal'), {backdrop: 'static'});
modal.show();
// Async: load extracted data and pre-fill form
await qvLoadAndPrefill(fileId);
}
async function qvLoadAndPrefill(fileId, isRetry) {
const statusEl = document.getElementById('qvStatusAlert');
statusEl.className = 'alert alert-info py-2 small';
statusEl.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Henter fakturadata…';
try {
const resp = await fetch(`/api/v1/supplier-invoices/files/${fileId}/extracted-data`);
if (!resp.ok) { throw new Error(`HTTP ${resp.status}`); }
const data = await resp.json();
console.log('[QV] extracted-data response:', JSON.stringify({
file_id: data.file_id,
status: data.status,
has_extraction: !!data.extraction,
has_llm_data: !!data.llm_data,
llm_data: data.llm_data,
extraction_vendor_name: data.extraction?.vendor_name,
extraction_vendor_cvr: data.extraction?.vendor_cvr,
}));
// Normaliseret data fra server (backend bygger llm_data med rigtige feltnavne)
const ld = data.llm_data || {};
const ext = data.extraction || {};
// llm_response_json: kan være JSONB-objekt eller string
let rawAi = {};
const rawLlm = ext.llm_response_json;
if (rawLlm) {
rawAi = (typeof rawLlm === 'string') ? (() => { try { return JSON.parse(rawLlm); } catch(e) { return {}; } })() : rawLlm;
}
console.log('[QV] rawAi keys:', Object.keys(rawAi).join(', ') || '(tom)');
// Hent vendor-felter fra alle 3 kilder i prioriteret rækkefølge
const name = ld.vendor_name || ext.vendor_name || rawAi.vendor_name || rawAi.issuer || '';
const cvr = (ld.vendor_cvr || ext.vendor_cvr || rawAi.vendor_cvr || rawAi.vendor_vat || '').toString().replace(/^DK/i, '').trim();
const email = ld.vendor_email || rawAi.vendor_email || rawAi.supplier_email || '';
const addr = ld.vendor_address || rawAi.vendor_address || rawAi.supplier_address || rawAi.vendor_street || '';
const postal = ld.vendor_postal_code || rawAi.vendor_postal_code || rawAi.postal_code || '';
const city = ld.vendor_city || rawAi.vendor_city || rawAi.city || '';
console.log('[QV] Parsed fields:', { name, cvr, email, addr, postal, city });
// Ingen extraction i DB overhovedet (fil aldrig kørt) → auto-reprocess
if (!data.extraction && !isRetry) {
console.log('[QV] Ingen extraction starter auto-reprocess');
await qvAutoReprocess(fileId);
return;
}
// Extraction findes men ingen vendor data → tilbyd reprocess
if (!name && !cvr && !isRetry) {
console.log('[QV] Extraction uden vendor data starter auto-reprocess');
await qvAutoReprocess(fileId);
return;
}
// Udfyld form
if (name) document.getElementById('qvName').value = name;
if (cvr) document.getElementById('qvCVR').value = cvr;
if (email) document.getElementById('qvEmail').value = email;
if (addr) {
const parts = addr.split(/,|\n/).map(s => s.trim()).filter(Boolean);
if (parts.length >= 1) document.getElementById('qvAddress').value = parts[0];
if (!postal && !city && parts.length >= 2) {
const postalCity = parts[parts.length - 1];
const m = postalCity.match(/^(\d{4})\s+(.+)$/);
if (m) { document.getElementById('qvPostal').value = m[1]; document.getElementById('qvCity').value = m[2]; }
else { document.getElementById('qvCity').value = postalCity; }
}
}
if (postal) document.getElementById('qvPostal').value = postal;
if (city) document.getElementById('qvCity').value = city;
if (name || cvr) {
statusEl.className = 'alert alert-success py-2 small';
statusEl.textContent = `✅ Data hentet${name ? ': ' + name : ''}${cvr ? ' (' + cvr + ')' : ''}`;
setTimeout(() => { statusEl.className = 'alert d-none py-2 small'; }, 4000);
} else {
// AI fandt ingen vendor men vis hvad der er (fakturanr, beløb)
const inv = ld.invoice_number || rawAi.invoice_number || '';
const amt = ld.total_amount || rawAi.total_amount || '';
statusEl.className = 'alert alert-warning py-2 small';
statusEl.innerHTML = `AI fandt ingen leverandørdata${inv ? ' (Faktura ' + inv + (amt ? ', ' + amt + ' DKK' : '') + ')' : ''}. Udfyld navn manuelt eller søg herover.`;
}
} catch(e) {
console.error('[QV] Fejl:', e);
statusEl.className = 'alert alert-danger py-2 small';
statusEl.textContent = 'Fejl ved hentning: ' + e.message;
}
}
async function qvAutoReprocess(fileId) {
const statusEl = document.getElementById('qvStatusAlert');
statusEl.className = 'alert alert-info py-2 small';
statusEl.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Analyserer faktura med AI vent venligst…';
try {
console.log('[QV] Starter reprocess for file:', fileId);
const r = await fetch(`/api/v1/supplier-invoices/reprocess/${fileId}`, { method: 'POST' });
if (!r.ok) {
const errBody = await r.text();
console.error('[QV] Reprocess fejlede:', r.status, errBody);
throw new Error(`Reprocess HTTP ${r.status}: ${errBody}`);
}
const reprocessResult = await r.json();
console.log('[QV] Reprocess result:', JSON.stringify(reprocessResult));
// Hent opdateret data med isRetry=true for at undgå uendelig løkke
await qvLoadAndPrefill(fileId, true);
loadUnhandledFiles();
} catch(e) {
console.error('[QV] Auto-reprocess fejl:', e);
statusEl.className = 'alert alert-warning py-2 small';
statusEl.innerHTML = `Kunne ikke køre AI-analyse: ${e.message}. <button class="btn btn-sm btn-outline-warning ms-2" onclick="qvAutoReprocess(${fileId})">Prøv igen</button>`;
}
}
async function qvSearchVendors(query) {
const results = document.getElementById('qvSearchResults');
if (!query || query.length < 2) {
results.innerHTML = '<div class="list-group-item text-muted small py-1">Søg for at finde eksisterende leverandør</div>';
return;
}
try {
const resp = await fetch(`/api/v1/vendors?search=${encodeURIComponent(query)}&active_only=true`);
const vendors = await resp.json();
if (!vendors || vendors.length === 0) {
results.innerHTML = '<div class="list-group-item text-muted small py-1">Ingen leverandører fundet</div>';
return;
}
results.innerHTML = vendors.slice(0, 10).map(v => `
<button type="button" class="list-group-item list-group-item-action py-1 small"
onclick="qvSelectVendor(${v.id}, '${escapeHtml(v.name)}', '${v.cvr_number || ''}')">
<strong>${escapeHtml(v.name)}</strong>
${v.cvr_number ? `<span class="text-muted ms-2">${v.cvr_number}</span>` : ''}
</button>
`).join('');
} catch(e) {
results.innerHTML = '<div class="list-group-item text-danger small py-1">Fejl ved søgning</div>';
}
}
function qvSelectVendor(vendorId, vendorName, vendorCVR) {
document.getElementById('qvExistingVendorId').value = vendorId;
document.getElementById('qvName').value = vendorName;
document.getElementById('qvCVR').value = vendorCVR;
const alert = document.getElementById('qvStatusAlert');
alert.className = 'alert alert-success py-2 small';
alert.textContent = `✅ Valgt: ${vendorName} — klik "Opret og link" for at linke`;
}
async function saveQuickVendor() {
const fileId = document.getElementById('qvFileId').value;
const existingId = document.getElementById('qvExistingVendorId').value;
const name = document.getElementById('qvName').value.trim();
const cvr = document.getElementById('qvCVR').value.trim();
const email = document.getElementById('qvEmail').value.trim();
const phone = document.getElementById('qvPhone').value.trim();
const address = document.getElementById('qvAddress').value.trim();
const postal = document.getElementById('qvPostal').value.trim();
const city = document.getElementById('qvCity').value.trim();
const domain = document.getElementById('qvDomain').value.trim();
const category = document.getElementById('qvCategory').value;
const notes = document.getElementById('qvNotes').value.trim();
const statusEl = document.getElementById('qvStatusAlert');
if (!name) {
statusEl.className = 'alert alert-danger py-2 small';
statusEl.textContent = 'Navn er påkrævet.';
return;
}
statusEl.className = 'alert alert-info py-2 small';
statusEl.textContent = 'Gemmer…';
try {
let vendorId = existingId ? parseInt(existingId) : null;
if (!vendorId) {
// Create new vendor
const payload = {
name, cvr_number: cvr || null,
email: email || null, phone: phone || null,
address: [address, postal && city ? `${postal} ${city}` : city].filter(Boolean).join('\n') || null,
postal_code: postal || null, city: city || null,
domain: domain || null, category,
notes: notes || null
};
const resp = await fetch('/api/v1/vendors', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || 'Oprettelse fejlede');
}
const created = await resp.json();
vendorId = created.id;
}
// Link vendor to file
const linkResp = await fetch(`/api/v1/supplier-invoices/files/${fileId}/link-vendor`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({vendor_id: vendorId})
});
if (!linkResp.ok) {
const err = await linkResp.json().catch(() => ({}));
throw new Error(err.detail || 'Link fejlede');
}
statusEl.className = 'alert alert-success py-2 small';
statusEl.textContent = `✅ Leverandør ${existingId ? 'linket' : 'oprettet og linket'}!`;
setTimeout(() => {
bootstrap.Modal.getInstance(document.getElementById('quickVendorSplitModal')).hide();
loadUnhandledFiles();
}, 900);
} catch(e) {
statusEl.className = 'alert alert-danger py-2 small';
statusEl.textContent = '❌ ' + e.message;
}
}
// Rerun full extraction for a file in the unhandled tab
async function rerunSingleFile(fileId) {
try {
showLoadingOverlay('Kører analyse...');
const response = await fetch(`/api/v1/supplier-invoices/reprocess/${fileId}`, {
method: 'POST'
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.detail || 'Analyse fejlede');
}
const result = await response.json();
hideLoadingOverlay();
const confPct = result.confidence ? Math.round(result.confidence * 100) + '%' : '?%';
const vendorInfo = result.vendor_id ? `Leverandør matchet (ID ${result.vendor_id})` : 'Ingen leverandør matchet';
alert(`✅ Analyse færdig\n${vendorInfo}\nConfidence: ${confPct}`);
loadUnhandledFiles();
} catch (error) {
hideLoadingOverlay();
console.error('Rerun error:', error);
alert('❌ Fejl ved analyse: ' + error.message);
}
}
// NEW: Analyze single file
async function analyzeFile(fileId) {
try {
showLoadingOverlay('Analyserer...');
// First match vendor
const matchResponse = await fetch(`/api/v1/supplier-invoices/files/${fileId}/match-vendor`, {
method: 'POST'
});
if (!matchResponse.ok) throw new Error('Vendor matching failed');
const matchResult = await matchResponse.json();
hideLoadingOverlay();
if (matchResult.requires_manual_selection) {
alert(`⚠️ Ingen 100% leverandør-match fundet.\n\nTop matches:\n` +
matchResult.matches.slice(0, 3).map(m =>
`- ${m.vendor_name} (${m.confidence}%): ${m.match_reason}`
).join('\n'));
loadUnhandledFiles(); // Reload to show vendor dropdown
} else {
alert(`✅ Leverandør matchet: ${matchResult.auto_selected.vendor_name}\n\nFilen er klar til næste trin.`);
loadUnhandledFiles();
}
} catch (error) {
hideLoadingOverlay();
console.error('Analysis error:', error);
alert('❌ Fejl ved analyse');
}
}
// NEW: Load kassekladde view (invoices ready for e-conomic)
async function loadKassekladdeView() {
try {
const tbody = document.getElementById('kassekladdeTable');
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
`;
// Load invoices that are unpaid/approved (kassekladde stage)
const response = await fetch('/api/v1/supplier-invoices?status=unpaid,approved&limit=100');
const data = await response.json();
const invoices = data.invoices || [];
// Update badge count
const badge = document.getElementById('kassekladdeCount');
if (invoices.length > 0) {
badge.textContent = invoices.length;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
renderKassekladdeTable(invoices);
} catch (error) {
console.error('Failed to load kassekladde:', error);
document.getElementById('kassekladdeTable').innerHTML = `
<tr>
<td colspan="9" class="text-center text-danger py-4">
<i class="bi bi-exclamation-triangle me-2"></i>
Fejl ved indlæsning
</td>
</tr>
`;
}
}
// NEW: Render kassekladde table
function renderKassekladdeTable(invoices) {
const tbody = document.getElementById('kassekladdeTable');
if (!invoices || invoices.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-muted py-4">
<i class="bi bi-inbox me-2"></i>
Ingen fakturaer i kassekladde
</td>
</tr>
`;
return;
}
tbody.innerHTML = invoices.map(inv => {
const lineCount = (inv.lines || []).length;
const statusBadge = getStatusBadge(inv.status);
return `
<tr>
<td>
<input type="checkbox" class="form-check-input kassekladde-checkbox"
data-invoice-id="${inv.id}"
onchange="updateKassekladdeBulkActions()">
</td>
<td>${inv.invoice_number || '-'}</td>
<td>${inv.vendor_full_name || inv.vendor_name || '-'}</td>
<td>${inv.invoice_date ? new Date(inv.invoice_date).toLocaleDateString('da-DK') : '-'}</td>
<td>${inv.due_date ? new Date(inv.due_date).toLocaleDateString('da-DK') : '-'}</td>
<td><strong>${formatCurrency(inv.total_amount)}</strong></td>
<td>
<span class="badge bg-secondary">${lineCount} linjer</span>
</td>
<td>${statusBadge}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="viewInvoiceDetails(${inv.id})" title="Detaljer">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-outline-success" onclick="sendSingleToEconomic(${inv.id})" title="Send til e-conomic">
<i class="bi bi-send"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
}
// NEW: Toggle select all kassekladde
function toggleSelectAllKassekladde() {
const checkbox = document.getElementById('selectAllKassekladde');
const checkboxes = document.querySelectorAll('.kassekladde-checkbox');
checkboxes.forEach(cb => cb.checked = checkbox.checked);
updateKassekladdeBulkActions();
}
// NEW: Update kassekladde bulk actions bar
function updateKassekladdeBulkActions() {
const checkboxes = document.querySelectorAll('.kassekladde-checkbox:checked');
const count = checkboxes.length;
const bar = document.getElementById('kassekladdeBulkActionsBar');
if (count > 0) {
bar.style.display = 'block';
document.getElementById('selectedKassekladdeCount').textContent = count;
} else {
bar.style.display = 'none';
}
}
// Load pending uploaded files
async function loadPendingFiles() {
try {
const tbody = document.getElementById('pendingFilesTable');
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Indlæser...</span>
</div>
</td>
</tr>
`;
const response = await fetch('/api/v1/pending-supplier-invoice-files');
const data = await response.json();
// Update badge count
const count = data.count || 0;
const badge = document.getElementById('pendingFilesCount');
if (count > 0) {
badge.textContent = count;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
renderPendingFiles(data.files || []);
} catch (error) {
console.error('Failed to load pending files:', error);
document.getElementById('pendingFilesTable').innerHTML = `
<tr>
<td colspan="8" class="text-center text-danger py-4">
<i class="bi bi-exclamation-triangle me-2"></i>
Fejl ved indlæsning af filer
</td>
</tr>
`;
}
}
// Render pending files table
function renderPendingFiles(files) {
const tbody = document.getElementById('pendingFilesTable');
if (!files || files.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-muted py-4">
<i class="bi bi-inbox me-2"></i>
Ingen filer mangler behandling
</td>
</tr>
`;
return;
}
tbody.innerHTML = files.map(file => {
// Check if this is BMC's own outgoing invoice
const isOwnInvoice = file.is_own_invoice === true;
const rowClass = isOwnInvoice ? 'table-danger' : (file.status === 'duplicate' ? 'table-danger' : '');
// Format Quick Analysis data
let quickAnalysisHtml = '<span class="text-muted">-</span>';
if (file.detected_cvr || file.detected_document_type || file.detected_document_number) {
quickAnalysisHtml = `<div class="small">`;
// OUTGOING INVOICE WARNING
if (isOwnInvoice) {
quickAnalysisHtml += `<div><span class="badge bg-danger"><i class="bi bi-exclamation-triangle me-1"></i>UDGÅENDE FAKTURA</span></div>`;
quickAnalysisHtml += `<div class="text-danger small mt-1"><strong>BMCs egen faktura - slet eller ignorer</strong></div>`;
}
if (file.detected_document_type) {
const isCredit = file.detected_document_type === 'credit_note';
quickAnalysisHtml += `<div><span class="badge ${isCredit ? 'bg-warning text-dark' : 'bg-primary'}">${isCredit ? '💳 Kreditnota' : '📄 Faktura'}</span></div>`;
}
if (file.detected_document_number) {
quickAnalysisHtml += `<div class="text-muted mt-1"><i class="bi bi-hash"></i> ${file.detected_document_number}</div>`;
}
if (file.detected_cvr) {
quickAnalysisHtml += `<div class="text-muted mt-1"><i class="bi bi-building"></i> CVR: ${file.detected_cvr}</div>`;
}
if (file.detected_vendor_name) {
quickAnalysisHtml += `<div class="text-success mt-1"><i class="bi bi-check-circle"></i> ${file.detected_vendor_name}</div>`;
}
quickAnalysisHtml += `</div>`;
}
return `
<tr class="${rowClass}">
<td>
<input type="checkbox" class="form-check-input file-checkbox" value="${file.file_id}" onchange="updateBulkActions()" ${file.status === 'duplicate' || isOwnInvoice ? 'disabled' : ''}>
</td>
<td>
<i class="bi bi-file-earmark-pdf text-danger me-2"></i>
<strong>${file.filename}</strong>
${isOwnInvoice ? `
<div class="mt-1">
<span class="badge bg-danger"><i class="bi bi-exclamation-triangle me-1"></i>UDGÅENDE FAKTURA (BMC)</span>
</div>
` : ''}
${file.status === 'duplicate' && file.error_message ? `
<div class="mt-1">
<span class="badge bg-danger"><i class="bi bi-exclamation-triangle me-1"></i>${file.error_message}</span>
</div>
` : ''}
</td>
<td>${formatDate(file.uploaded_at)}</td>
<td>${getFileStatusBadge(file.status)}</td>
<td>${quickAnalysisHtml}</td>
<td>
${file.vendor_matched_id ?
`<span class="badge bg-success" title="${file.matched_vendor_name}"><i class="bi bi-check-circle me-1"></i>${file.matched_vendor_name}</span>` :
(file.vendor_name ?
`<div class="d-flex align-items-center gap-2">
<span class="badge bg-warning text-dark" title="CVR: ${file.vendor_cvr || 'Ukendt'}">
<i class="bi bi-question-circle me-1"></i>${file.vendor_name}
</span>
<button class="btn btn-xs btn-outline-primary" onclick="linkOrCreateVendor(${file.file_id}, '${escapeHtml(file.vendor_name)}', '${file.vendor_cvr || ''}')" title="Link eller opret leverandør">
<i class="bi bi-link-45deg"></i>
</button>
</div>` :
'<span class="text-muted">-</span>')
}
</td>
<td>
${file.template_id ?
`<span class="badge bg-info"><i class="bi bi-file-text me-1"></i>Template #${file.template_id}</span>` :
(file.has_invoice2data_template ?
`<span class="badge bg-success"><i class="bi bi-file-earmark-code me-1"></i>Invoice2Data: ${file.invoice2data_template_name}</span>` :
(file.vendor_matched_id || file.detected_vendor_id ?
`<div class="d-flex flex-column gap-1">
<span class="badge bg-warning text-dark">
<i class="bi bi-exclamation-triangle me-1"></i>Ingen template
</span>
<a href="#" onclick="createTemplateForFile(${file.file_id}, ${file.vendor_matched_id || file.detected_vendor_id}, '${escapeHtml(file.matched_vendor_name || file.detected_vendor_name || 'leverandør')}'); return false;"
class="small text-primary" title="Opret template med data fra denne fil">
<i class="bi bi-plus-circle me-1"></i>Opret template for ${file.matched_vendor_name || file.detected_vendor_name || 'leverandør'}
</a>
</div>` :
'<span class="text-muted">-</span>'))
}
</td>
<td>
<div class="btn-group btn-group-sm">
${isOwnInvoice ? `
<button class="btn btn-outline-danger" disabled title="Udgående faktura - kan ikke behandles">
<i class="bi bi-x-circle me-1"></i>Udgående Faktura
</button>
` : ''}
${file.status === 'duplicate' && !isOwnInvoice ? `
<button class="btn btn-outline-secondary" disabled title="Dublet - kan ikke behandles">
<i class="bi bi-lock me-1"></i>Låst (Dublet)
</button>
` : ''}
${(file.status === 'ai_extracted' || file.status === 'processed') && !isOwnInvoice ? `
<button class="btn btn-outline-primary" onclick="reviewExtractedData(${file.file_id})" title="Gennemse udtrukne data">
<i class="bi bi-eye me-1"></i>Gennemse
</button>
` : ''}
${(file.status === 'failed' || file.status === 'pending') && !isOwnInvoice ? `
<button class="btn btn-outline-warning" onclick="retryExtraction(${file.file_id})" title="Behandl fil med AI">
<i class="bi bi-arrow-clockwise me-1"></i>Behandl
</button>
` : ''}
${file.status === 'processing' && !isOwnInvoice ? `
<button class="btn btn-outline-secondary" disabled>
<i class="bi bi-hourglass-split me-1"></i>Behandler...
</button>
` : ''}
${file.status !== 'duplicate' ? `
<a href="/api/v1/supplier-invoices/files/${file.file_id}/download" class="btn btn-outline-success" target="_blank" title="Se PDF">
<i class="bi bi-file-pdf me-1"></i>Se PDF
</a>
` : ''}
<button class="btn btn-outline-danger" onclick="deletePendingFile(${file.file_id})" title="Slet fil">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
}
// Get status badge for file status
function getFileStatusBadge(status) {
const badges = {
'pending': '<span class="badge status-pending"><i class="bi bi-clock me-1"></i>Afventer</span>',
'processing': '<span class="badge status-processing"><i class="bi bi-hourglass-split me-1"></i>Behandler</span>',
'ai_extracted': '<span class="badge status-ai_extracted"><i class="bi bi-robot me-1"></i>AI Udtrukket</span>',
'processed': '<span class="badge status-sent"><i class="bi bi-check-circle me-1"></i>Behandlet</span>',
'failed': '<span class="badge status-failed"><i class="bi bi-exclamation-triangle me-1"></i>Fejlet</span>',
'duplicate': '<span class="badge bg-danger"><i class="bi bi-x-circle me-1"></i>DUBLET</span>'
};
return badges[status] || `<span class="badge bg-secondary">${status}</span>`;
}
// Review extracted data from file
async function reviewExtractedData(fileId) {
try {
const response = await fetch(`/api/v1/supplier-invoices/files/${fileId}/extracted-data`);
const data = await response.json();
if (!data.extraction) {
alert('Ingen udtrukne data fundet. Prøv at behandle filen igen.');
return;
}
const ext = data.extraction;
const lines = data.extraction_lines || [];
// Parse JSON if llm_response_json exists
let aiData = null;
if (ext.llm_response_json) {
try {
aiData = JSON.parse(ext.llm_response_json);
} catch (e) {
console.error('Failed to parse llm_response_json:', e);
}
}
// Normalize CVR - remove DK prefix for display
const normalizedCVR = (ext.vendor_cvr || aiData?.vendor_cvr || '-')
.replace(/^DK/i, '')
.replace(/^dk/i, '')
.trim();
// Check for validation warnings
const hasValidationIssues = aiData?._validation_warning || aiData?._vat_warning;
const allValidationsPassed = !hasValidationIssues && aiData && aiData.lines && aiData.lines.length > 0;
// Build modal content
let modalContent = `
<div class="alert alert-info">
<strong>Fil:</strong> ${data.filename}<br>
<strong>Status:</strong> ${data.status}<br>
<strong>Confidence:</strong> ${ext.confidence ? (ext.confidence * 100).toFixed(0) + '%' : 'N/A'}<br>
${ext.vendor_matched_id ?
`<strong class="text-success">✓ Leverandør fundet i system (ID: ${ext.vendor_matched_id})</strong>` :
`<strong class="text-warning">⚠ Leverandør ikke fundet - skal oprettes</strong>`
}
</div>
${hasValidationIssues ? `
<div class="alert alert-warning">
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-2"></i>Beløbs-validering</h6>
${aiData._validation_warning ? `<p class="mb-1">⚠️ ${aiData._validation_warning}</p>` : ''}
${aiData._vat_warning ? `<p class="mb-0">⚠️ ${aiData._vat_warning}</p>` : ''}
</div>
` : allValidationsPassed ? `
<div class="alert alert-success">
<h6 class="alert-heading"><i class="bi bi-check-circle me-2"></i>Beløbs-validering</h6>
<p class="mb-0">✅ Varelinjer summer korrekt til subtotal<br>✅ Moms beregning er korrekt (25%)</p>
</div>
` : ''}
<h5>Udtrukne Data:</h5>
<table class="table table-sm">
<tr><th>Felt</th><th>Værdi</th></tr>
<tr><td>Fakturanummer</td><td><strong>${aiData?.invoice_number || ext.document_id || '-'}</strong></td></tr>
<tr><td>Fakturadato</td><td>${ext.document_date || aiData?.invoice_date || '-'}</td></tr>
<tr><td>Forfaldsdato</td><td>${ext.due_date || aiData?.due_date || '-'}</td></tr>
<tr><td>Total beløb</td><td><strong>${ext.total_amount || aiData?.total_amount || '-'} ${ext.currency || aiData?.currency || 'DKK'}</strong></td></tr>
<tr><td>Leverandør navn</td><td>${ext.vendor_name || aiData?.vendor_name || '-'}</td></tr>
<tr><td>Leverandør CVR</td><td><strong>${normalizedCVR}</strong></td></tr>
<tr><td>Leverandør adresse</td><td>${aiData?.vendor_address || '-'}</td></tr>
</table>
${lines.length > 0 || (aiData?.line_items && aiData.line_items.length > 0) || (aiData?.lines && aiData.lines.length > 0) ? `
<h6>Fakturalinjer:</h6>
<table class="table table-sm">
<thead>
<tr><th>Beskrivelse</th><th>Antal</th><th>Pris</th><th>Total</th></tr>
</thead>
<tbody>
${(lines.length > 0 ? lines : (aiData.line_items || aiData.lines || [])).map(line => `
<tr>
<td>${line.description || '-'}</td>
<td>${line.quantity || '-'}</td>
<td>${line.unit_price || '-'}</td>
<td>${line.total_amount || line.total_price || line.line_total || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
` : '<p class="text-muted">Ingen linjer fundet</p>'}
${data.pdf_text_preview && data.pdf_text_preview.trim() ? `
<h6 class="mt-3">PDF Tekst Preview:</h6>
<div class="border rounded p-3 bg-body-secondary" style="max-height: 500px; overflow-y: auto;">
<pre class="mb-0 text-body" style="font-size: 0.85rem; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; line-height: 1.3;">${escapeHtml(data.pdf_text_preview)}</pre>
</div>
` : '<div class="alert alert-warning mt-3"><i class="bi bi-exclamation-triangle me-2"></i>PDF tekst ikke tilgængelig - prøv at genbehandle filen</div>'}
`;
document.getElementById('reviewModalContent').innerHTML = modalContent;
document.getElementById('reviewModalFileId').value = fileId;
new bootstrap.Modal(document.getElementById('reviewExtractedDataModal')).show();
} catch (error) {
console.error('Failed to load extracted data:', error);
alert('Kunne ikke hente udtrukne data: ' + error.message);
}
}
// Retry extraction for failed file
async function retryExtraction(fileId) {
try {
if (!confirm('Vil du prøve at udtrække data fra denne fil igen?')) return;
const response = await fetch(`/api/v1/supplier-invoices/reprocess/${fileId}`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
// Reload pending files list to show updated status
await loadPendingFiles();
// Show warning if no template was matched
let message = 'Fil behandlet med succes!';
if (result.warning) {
message = result.warning + '\n\n' + message + '\n\nOvervej at oprette en template for hurtigere behandling.';
}
// Show success message and offer to review
if (confirm(message + '\n\nVil du gennemse de udtrukne data?')) {
reviewExtractedData(fileId);
}
} else {
const error = await response.json();
alert('Kunne ikke behandle fil: ' + (error.detail || 'Ukendt fejl'));
}
} catch (error) {
console.error('Failed to retry extraction:', error);
alert('Kunne ikke prøve igen: ' + error.message);
}
}
// Reprocess from review modal
async function reprocessFromModal() {
const fileId = document.getElementById('reviewModalFileId').value;
if (!fileId) {
alert('Ingen fil valgt');
return;
}
try {
if (!confirm('Vil du genbehandle denne fil? Den nuværende udtrækning vil blive overskrevet.')) return;
// Show loading in modal
document.getElementById('reviewModalContent').innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary mb-3" role="status"></div>
<p>Genbehandler fil...</p>
</div>
`;
const response = await fetch(`/api/v1/supplier-invoices/reprocess/${fileId}`, {
method: 'POST'
});
if (response.ok) {
// Reload the extracted data in the modal
await reviewExtractedData(fileId);
// Also reload pending files list
await loadPendingFiles();
} else {
const error = await response.json();
alert('Kunne ikke genbehandle fil: ' + (error.detail || 'Ukendt fejl'));
}
} catch (error) {
console.error('Failed to reprocess:', error);
alert('Kunne ikke genbehandle: ' + error.message);
}
}
// Delete pending file
async function deletePendingFile(fileId) {
try {
if (!confirm('Er du sikker på at du vil slette denne fil? Dette kan ikke fortrydes.')) return;
const response = await fetch(`/api/v1/supplier-invoices/files/${fileId}`, {
method: 'DELETE'
});
if (response.ok) {
alert('Fil slettet!');
loadPendingFiles(); // Reload list
} else {
const error = await response.json();
alert('Kunne ikke slette fil: ' + (error.detail || 'Ukendt fejl'));
}
} catch (error) {
console.error('Failed to delete file:', error);
alert('Kunne ikke slette fil: ' + error.message);
}
}
// Create template for file
async function createTemplateForFile(fileId, vendorId, vendorName) {
try {
// Get file data including PDF text
const response = await fetch(`/api/v1/supplier-invoices/files/${fileId}/extracted-data`);
if (!response.ok) {
throw new Error('Kunne ikke hente fil data');
}
const data = await response.json();
const pdfText = data.pdf_text || '';
// Store data in sessionStorage for template creation page
sessionStorage.setItem('templateCreateData', JSON.stringify({
vendorId: vendorId,
vendorName: vendorName,
fileId: fileId,
pdfText: pdfText,
sampleInvoice: data.llm_data || {}
}));
// Navigate to template builder page
window.location.href = `/billing/template-builder?vendor=${vendorId}&file=${fileId}`;
} catch (error) {
console.error('Failed to prepare template creation:', error);
alert('Kunne ikke forberede template oprettelse: ' + error.message);
}
}
// Link or create vendor for extraction
let selectedVendorId = null;
function linkOrCreateVendor(fileId, vendorName, vendorCVR) {
selectedVendorId = null;
document.getElementById('linkVendorFileId').value = fileId;
document.getElementById('linkVendorName').value = vendorName;
document.getElementById('linkVendorCVR').value = vendorCVR;
document.getElementById('linkVendorDisplayName').textContent = vendorName;
document.getElementById('linkVendorDisplayCVR').textContent = vendorCVR || 'Ikke fundet';
// Pre-fill create form
document.getElementById('newVendorName').value = vendorName;
document.getElementById('newVendorCVR').value = vendorCVR;
// Reset search
document.getElementById('vendorSearchInput').value = '';
document.getElementById('vendorSearchResults').innerHTML = '<div class="text-center text-muted py-3">Søg for at finde leverandører</div>';
// Show modal
const modal = new bootstrap.Modal(document.getElementById('linkVendorModal'));
modal.show();
}
// Search vendors in link modal
let vendorSearchTimer;
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('vendorSearchInput');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
clearTimeout(vendorSearchTimer);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('vendorSearchResults').innerHTML = '<div class="text-center text-muted py-3">Indtast mindst 2 tegn</div>';
return;
}
vendorSearchTimer = setTimeout(() => searchVendorsForLink(query), 300);
});
}
// Tab switching
const tabs = document.querySelectorAll('[data-bs-toggle="tab"]');
tabs.forEach(tab => {
tab.addEventListener('shown.bs.tab', (e) => {
const target = e.target.getAttribute('data-bs-target');
if (target === '#createNew') {
document.getElementById('linkExistingBtn').style.display = 'none';
document.getElementById('createNewBtn').style.display = 'inline-block';
} else {
document.getElementById('linkExistingBtn').style.display = 'inline-block';
document.getElementById('createNewBtn').style.display = 'none';
}
});
});
});
async function searchVendorsForLink(query) {
try {
const response = await fetch(`/api/v1/vendors?search=${encodeURIComponent(query)}&limit=20`);
const vendors = await response.json(); // API returns array directly
const resultsDiv = document.getElementById('vendorSearchResults');
if (!vendors || vendors.length === 0) {
resultsDiv.innerHTML = '<div class="text-center text-muted py-3">Ingen leverandører fundet</div>';
return;
}
resultsDiv.innerHTML = vendors.map(v => `
<button type="button" class="list-group-item list-group-item-action ${selectedVendorId === v.id ? 'active' : ''}"
onclick="selectVendorForLink(${v.id}, '${escapeHtml(v.name)}')">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>${v.name}</strong>
${v.cvr_number ? `<br><small class="text-muted">CVR: ${v.cvr_number}</small>` : ''}
</div>
${selectedVendorId === v.id ? '<i class="bi bi-check-circle text-success"></i>' : ''}
</div>
</button>
`).join('');
} catch (error) {
console.error('Vendor search failed:', error);
document.getElementById('vendorSearchResults').innerHTML = '<div class="text-center text-danger py-3">Søgning fejlede</div>';
}
}
function selectVendorForLink(vendorId, vendorName) {
selectedVendorId = vendorId;
// Re-render to show selection
const searchInputElem = document.getElementById('vendorSearchInput');
if (searchInputElem && searchInputElem.value) {
searchVendorsForLink(searchInputElem.value);
}
}
async function saveVendorLink() {
if (!selectedVendorId) {
alert('Vælg venligst en leverandør fra listen');
return;
}
try {
const fileId = document.getElementById('linkVendorFileId').value;
// Update extraction with vendor_matched_id
const response = await fetch(`/api/v1/supplier-invoices/files/${fileId}/link-vendor`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({vendor_id: selectedVendorId})
});
if (response.ok) {
alert('Leverandør linket!');
bootstrap.Modal.getInstance(document.getElementById('linkVendorModal')).hide();
loadPendingFiles();
} else {
const error = await response.json();
alert('Kunne ikke linke leverandør: ' + (error.detail || 'Ukendt fejl'));
}
} catch (error) {
console.error('Link vendor failed:', error);
alert('Kunne ikke linke leverandør: ' + error.message);
}
}
async function createAndLinkVendor() {
const form = document.getElementById('createVendorForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
try {
const fileId = document.getElementById('linkVendorFileId').value;
// Create vendor
const vendorData = {
name: document.getElementById('newVendorName').value,
cvr_number: document.getElementById('newVendorCVR').value || null,
email: document.getElementById('newVendorEmail').value || null,
phone: document.getElementById('newVendorPhone').value || null,
address: document.getElementById('newVendorAddress').value || null
};
const createResp = await fetch('/api/v1/vendors', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(vendorData)
});
if (!createResp.ok) {
const error = await createResp.json();
alert('Kunne ikke oprette leverandør: ' + (error.detail || 'Ukendt fejl'));
return;
}
const newVendor = await createResp.json();
// Link vendor
const linkResp = await fetch(`/api/v1/supplier-invoices/files/${fileId}/link-vendor`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({vendor_id: newVendor.id})
});
if (linkResp.ok) {
alert('Leverandør oprettet og linket!');
bootstrap.Modal.getInstance(document.getElementById('linkVendorModal')).hide();
loadPendingFiles();
} else {
const error = await linkResp.json();
alert('Leverandør oprettet, men kunne ikke linkes: ' + (error.detail || 'Ukendt fejl'));
}
} catch (error) {
console.error('Create and link vendor failed:', error);
alert('Kunne ikke oprette leverandør: ' + error.message);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Create invoice from extracted data
async function createInvoiceFromExtraction() {
try {
const fileId = document.getElementById('reviewModalFileId').value;
if (!confirm('Vil du oprette en leverandørfaktura fra disse udtrukne data?')) return;
// Show loading state
const modal = document.getElementById('reviewModal');
const originalContent = document.getElementById('reviewModalContent').innerHTML;
document.getElementById('reviewModalContent').innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary mb-3"></div>
<p>Opretter faktura...</p>
</div>
`;
const response = await fetch(`/api/v1/supplier-invoices/from-extraction/${fileId}`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
alert(`✅ Faktura oprettet!\n\nFakturanummer: ${result.invoice_number}\nLeverandør: ${result.vendor_name}\nBeløb: ${result.total_amount} ${result.currency}`);
// Close modal and refresh
const modalInstance = bootstrap.Modal.getInstance(modal);
if (modalInstance) {
modalInstance.hide();
}
loadPendingFiles();
loadInvoices();
loadStats();
} else {
const error = await response.json();
document.getElementById('reviewModalContent').innerHTML = originalContent;
alert('Kunne ikke oprette faktura: ' + (error.detail || 'Ukendt fejl'));
}
} catch (error) {
console.error('Failed to create invoice:', error);
alert('Kunne ikke oprette faktura: ' + error.message);
}
}
// ========== MANUAL ENTRY MODE ==========
let manualLineCounter = 0;
async function openManualEntryMode() {
try {
console.log('Opening manual entry mode...');
const fileId = document.getElementById('reviewModalFileId').value;
console.log('File ID:', fileId);
if (!fileId) {
alert('Ingen fil ID fundet');
return;
}
// Set file ID
document.getElementById('manualEntryFileId').value = fileId;
// Clear form
document.getElementById('manualEntryForm').reset();
document.getElementById('manualLineItems').innerHTML = '';
manualLineCounter = 0;
// Close review modal
const reviewModal = bootstrap.Modal.getInstance(document.getElementById('reviewExtractedDataModal'));
if (reviewModal) {
reviewModal.hide();
}
// Open manual entry modal first
console.log('Opening manual entry modal...');
const manualModal = new bootstrap.Modal(document.getElementById('manualEntryModal'));
manualModal.show();
// Wait a bit for modal to render
await new Promise(resolve => setTimeout(resolve, 300));
// Load PDF text after modal is open
console.log('Loading PDF text...');
try {
const pdfResponse = await fetch(`/api/v1/supplier-invoices/files/${fileId}/pdf-text`);
if (pdfResponse.ok) {
const pdfData = await pdfResponse.json();
document.getElementById('pdfTextView').innerHTML = `<pre class="mb-0 text-body" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(pdfData.pdf_text)}</pre>`;
} else {
document.getElementById('pdfTextView').innerHTML = '<div class="text-danger">Kunne ikke indlæse PDF tekst</div>';
}
} catch (e) {
console.error('Error loading PDF text:', e);
document.getElementById('pdfTextView').innerHTML = '<div class="text-danger">Fejl ved indlæsning af PDF</div>';
}
// Also set iframe src for original view toggle
document.getElementById('manualEntryPdfViewer').src = `/api/v1/supplier-invoices/files/${fileId}/pdf`;
// Load vendors into dropdown
console.log('Loading vendors...');
await loadVendorsForManual();
// Load extracted data and prefill form
console.log('Loading extracted data...');
try {
const response = await fetch(`/api/v1/supplier-invoices/files/${fileId}/extracted-data`);
if (response.ok) {
const data = await response.json();
console.log('Extracted data:', data);
// Prefill form fields
if (data.llm_data) {
const llm = data.llm_data;
console.log('LLM data invoice_number:', llm.invoice_number);
// Invoice number
if (llm.invoice_number) {
console.log('Setting invoice number:', llm.invoice_number);
document.getElementById('manualInvoiceNumber').value = llm.invoice_number;
} else {
console.warn('No invoice_number in llm_data');
}
// Invoice date
if (llm.invoice_date) {
document.getElementById('manualInvoiceDate').value = llm.invoice_date;
}
// Due date
if (llm.due_date) {
document.getElementById('manualDueDate').value = llm.due_date;
}
// Total amount
if (llm.total_amount) {
document.getElementById('manualTotalAmount').value = Math.abs(llm.total_amount);
}
// Currency
if (llm.currency) {
document.getElementById('manualCurrency').value = llm.currency;
}
// Invoice type (check if credit note)
if (llm.document_type === 'credit_note') {
document.getElementById('manualInvoiceType').value = 'credit_note';
}
// Vendor - select if matched
if (data.vendor_matched_id) {
document.getElementById('manualVendorId').value = data.vendor_matched_id;
}
// Add line items
if (llm.lines && llm.lines.length > 0) {
llm.lines.forEach(line => {
addManualLine();
const lineNum = manualLineCounter;
// SKU
if (line.sku) {
document.getElementById(`manualLineSku${lineNum}`).value = line.sku;
}
// Description
if (line.description) {
let desc = line.description;
// Add VAT note to description if present
if (line.vat_note === 'reverse_charge') {
desc += ' ⚠️ OMVENDT BETALINGSPLIGT';
} else if (line.vat_note === 'copydan_included') {
desc += ' [Copydan incl.]';
}
document.getElementById(`manualLineDesc${lineNum}`).value = desc;
}
// Quantity
if (line.quantity) {
document.getElementById(`manualLineQty${lineNum}`).value = line.quantity;
}
// Unit price
if (line.unit_price) {
document.getElementById(`manualLinePrice${lineNum}`).value = Math.abs(line.unit_price);
}
// VAT code - auto-select based on vat_note
const vatCodeSelect = document.getElementById(`manualLineVatCode${lineNum}`);
if (line.vat_note === 'reverse_charge') {
vatCodeSelect.value = 'I52';
} else if (line.vat_rate === 0) {
vatCodeSelect.value = 'I0';
} else {
vatCodeSelect.value = 'I25';
}
// Contra account
if (line.contra_account) {
document.getElementById(`manualLineContra${lineNum}`).value = line.contra_account;
}
});
} else {
// Add one empty line if no lines extracted
addManualLine();
}
console.log('✅ Form prefilled with extracted data');
} else {
// No extracted data, add empty line
addManualLine();
}
} else {
console.warn('⚠️ Could not load extracted data, starting with empty form');
addManualLine();
}
} catch (error) {
console.error('Error loading extracted data:', error);
// Add empty line on error
addManualLine();
}
console.log('Manual entry modal opened successfully');
} catch (error) {
console.error('Error opening manual entry mode:', error);
alert('Fejl ved åbning af manuel indtastning: ' + error.message);
}
}
// Setup text selection feature for manual entry
function setupManualEntryTextSelection() {
// Track selection anywhere in document
document.addEventListener('mouseup', (e) => {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText && selectedText.length > 0) {
// Check if selection is from PDF text view
const pdfTextView = document.getElementById('pdfTextView');
if (pdfTextView && pdfTextView.contains(selection.anchorNode)) {
lastSelectedText = selectedText;
console.log('✅ Selected from PDF:', selectedText);
// Visual feedback
const tip = document.querySelector('.alert-info');
if (tip && tip.closest('#manualEntryModal')) {
tip.classList.remove('alert-info');
tip.classList.add('alert-success');
tip.innerHTML = `✅ <strong>"${selectedText.substring(0, 50)}${selectedText.length > 50 ? '...' : ''}"</strong> markeret - klik på et felt for at indsætte`;
setTimeout(() => {
tip.classList.remove('alert-success');
tip.classList.add('alert-info');
tip.innerHTML = '💡 <strong>Tip:</strong> Markér tekst og klik på et felt for at indsætte';
}, 3000);
}
}
}
});
// Insert selected text on field click/focus
document.addEventListener('click', (e) => {
// Check if clicked element is an input/textarea in the modal
if (lastSelectedText && e.target.closest('#manualEntryModal')) {
if ((e.target.tagName === 'INPUT' &&
e.target.type !== 'button' &&
e.target.type !== 'submit' &&
e.target.type !== 'checkbox' &&
e.target.type !== 'radio') ||
e.target.tagName === 'TEXTAREA') {
console.log('🎯 Clicked field:', e.target.id || e.target.name, 'Current value:', e.target.value);
console.log('📝 Will insert:', lastSelectedText);
// Only insert if field is empty or user confirms
if (!e.target.value || e.target.value.trim() === '') {
e.target.value = lastSelectedText;
console.log('✅ Auto-inserted into', e.target.id || e.target.name);
e.target.focus();
// Clear selection
lastSelectedText = '';
window.getSelection().removeAllRanges();
} else {
// Ask to replace
if (confirm(`Erstat "${e.target.value}" med "${lastSelectedText}"?`)) {
e.target.value = lastSelectedText;
console.log('✅ Replaced content in', e.target.id || e.target.name);
// Clear selection
lastSelectedText = '';
window.getSelection().removeAllRanges();
}
}
}
}
}, true); // Use capture phase
}
// Toggle between PDF text view and original PDF
function togglePdfView() {
const textView = document.getElementById('pdfTextView');
const pdfView = document.getElementById('manualEntryPdfViewer');
const icon = document.getElementById('pdfViewIcon');
if (textView.style.display === 'none') {
// Show text view
textView.style.display = 'block';
pdfView.style.display = 'none';
icon.className = 'bi bi-file-earmark-text';
} else {
// Show PDF view
textView.style.display = 'none';
pdfView.style.display = 'block';
icon.className = 'bi bi-file-earmark-pdf';
}
}
async function loadVendorsForManual() {
try {
console.log('Fetching vendors from API...');
const response = await fetch('/api/v1/vendors');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const vendors = await response.json();
console.log(`Loaded ${vendors.length} vendors`);
const select = document.getElementById('manualVendorId');
if (!select) {
throw new Error('manualVendorId element not found');
}
select.innerHTML = '<option value="">Vælg leverandør...</option>';
vendors.forEach(vendor => {
const option = document.createElement('option');
option.value = vendor.id;
option.textContent = `${vendor.name}${vendor.cvr_number ? ' (CVR: ' + vendor.cvr_number + ')' : ''}`;
select.appendChild(option);
});
console.log('Vendors loaded successfully');
} catch (error) {
console.error('Failed to load vendors:', error);
alert('Kunne ikke hente leverandører: ' + error.message);
}
}
function addManualLine() {
try {
manualLineCounter++;
console.log('Adding manual line', manualLineCounter);
const lineHtml = `
<div class="card mb-2" id="manualLine${manualLineCounter}">
<div class="card-body p-2">
<div class="row g-2 align-items-center">
<div class="col-md-1">
<label class="form-label mb-0 small text-muted">Varenr</label>
<input type="text" class="form-control form-control-sm" placeholder="SKU"
id="manualLineSku${manualLineCounter}" name="line_sku[]">
</div>
<div class="col-md-3">
<label class="form-label mb-0 small text-muted">Beskrivelse</label>
<input type="text" class="form-control form-control-sm" placeholder="Beskrivelse"
id="manualLineDesc${manualLineCounter}" name="line_description[]">
</div>
<div class="col-md-1">
<label class="form-label mb-0 small text-muted">Antal</label>
<input type="number" class="form-control form-control-sm" placeholder="Antal"
id="manualLineQty${manualLineCounter}" name="line_quantity[]" value="1" step="1">
</div>
<div class="col-md-2">
<label class="form-label mb-0 small text-muted">Enhedspris</label>
<input type="number" class="form-control form-control-sm" placeholder="Pris"
id="manualLinePrice${manualLineCounter}" name="line_price[]" step="0.01">
</div>
<div class="col-md-2">
<label class="form-label mb-0 small text-muted">Momskode</label>
<select class="form-select form-select-sm" id="manualLineVatCode${manualLineCounter}" name="line_vat_code[]">
<option value="I25" selected>I25 - 25% moms</option>
<option value="I52">I52 - Omvendt betalingspligt</option>
<option value="I0">I0 - 0% moms (momsfri)</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label mb-0 small text-muted">Modkonto</label>
<input type="text" class="form-control form-control-sm" placeholder="5810"
id="manualLineContra${manualLineCounter}" name="line_contra[]" value="5810">
</div>
<div class="col-md-1">
<label class="form-label mb-0 small text-muted">&nbsp;</label>
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="removeManualLine(${manualLineCounter})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
`;
const container = document.getElementById('manualLineItems');
if (!container) {
throw new Error('manualLineItems container not found');
}
container.insertAdjacentHTML('beforeend', lineHtml);
console.log('Manual line added successfully');
} catch (error) {
console.error('Error adding manual line:', error);
alert('Fejl ved tilføjelse af linje: ' + error.message);
}
}
function removeManualLine(lineId) {
document.getElementById(`manualLine${lineId}`).remove();
}
async function saveManualInvoice() {
try {
const fileId = document.getElementById('manualEntryFileId').value;
const editingInvoiceId = document.getElementById('manualEntryForm').dataset.editingInvoiceId;
const vendorId = document.getElementById('manualVendorId').value;
const invoiceNumber = document.getElementById('manualInvoiceNumber').value;
const invoiceDate = document.getElementById('manualInvoiceDate').value;
const dueDate = document.getElementById('manualDueDate').value;
const totalAmount = parseFloat(document.getElementById('manualTotalAmount').value);
const currency = document.getElementById('manualCurrency').value;
const invoiceType = document.getElementById('manualInvoiceType').value;
const notes = document.getElementById('manualNotes').value;
// Validate required fields
if (!vendorId || !invoiceNumber || !invoiceDate || !totalAmount) {
alert('Udfyld venligst alle påkrævede felter (markeret med *)');
return;
}
// Collect line items
const skus = document.getElementsByName('line_sku[]');
const descriptions = document.getElementsByName('line_description[]');
const quantities = document.getElementsByName('line_quantity[]');
const prices = document.getElementsByName('line_price[]');
const vatCodes = document.getElementsByName('line_vat_code[]');
const contraAccounts = document.getElementsByName('line_contra[]');
const lines = [];
for (let i = 0; i < descriptions.length; i++) {
if (descriptions[i].value.trim()) {
const qty = parseFloat(quantities[i].value) || 1;
const price = parseFloat(prices[i].value) || 0;
const vatCode = vatCodes[i].value;
// Determine VAT rate from code
let vatRate = 25.00;
if (vatCode === 'I52' || vatCode === 'I0') {
vatRate = 0.00;
}
lines.push({
line_number: i + 1,
sku: skus[i].value.trim() || null,
description: descriptions[i].value,
quantity: qty,
unit_price: price,
line_total: qty * price,
vat_code: vatCode,
vat_rate: vatRate,
contra_account: contraAccounts[i].value.trim() || '5810'
});
}
}
let response;
if (editingInvoiceId) {
// UPDATE existing invoice
response = await fetch(`/api/v1/supplier-invoices/${editingInvoiceId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
vendor_id: parseInt(vendorId),
invoice_number: invoiceNumber,
invoice_date: invoiceDate,
due_date: dueDate || invoiceDate,
total_amount: totalAmount,
currency: currency,
invoice_type: invoiceType,
notes: notes,
lines: lines
})
});
} else {
// CREATE new invoice
response = await fetch('/api/v1/supplier-invoices', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
vendor_id: parseInt(vendorId),
invoice_number: invoiceNumber,
invoice_date: invoiceDate,
due_date: dueDate || invoiceDate,
total_amount: totalAmount,
currency: currency,
invoice_type: invoiceType,
status: invoiceType === 'credit_note' ? 'credited' : 'unpaid',
notes: fileId ? `Manuel indtastning fra fil ID ${fileId}. ${notes}`.trim() : notes,
lines: lines
})
});
}
if (response.ok) {
const result = await response.json();
// Mark file as completed if this was from file processing
if (fileId) {
await fetch(`/api/v1/supplier-invoices/files/${fileId}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({status: 'completed'})
});
}
// Close modal and refresh
bootstrap.Modal.getInstance(document.getElementById('manualEntryModal')).hide();
if (editingInvoiceId) {
alert('✅ Faktura opdateret!');
} else {
alert(`${invoiceType === 'credit_note' ? 'Kreditnota' : 'Faktura'} oprettet!`);
}
// Clear editing flag
delete document.getElementById('manualEntryForm').dataset.editingInvoiceId;
// Refresh views
loadPendingFiles();
loadPaymentView();
loadReadyForBookingView();
loadLineItems();
loadStats();
} else {
const error = await response.json();
alert('Kunne ikke gemme faktura: ' + (error.detail || 'Ukendt fejl'));
}
} catch (error) {
console.error('Failed to save manual invoice:', error);
alert('Kunne ikke gemme faktura: ' + error.message);
}
}
// ========== END MANUAL ENTRY MODE ==========
// ========== BULK ACTIONS FOR PENDING FILES ==========
// Toggle select all checkboxes
function toggleSelectAll() {
const selectAll = document.getElementById('selectAllFiles');
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => cb.checked = selectAll.checked);
updateBulkActions();
}
// Update bulk actions bar visibility and count
function updateBulkActions() {
const checkboxes = document.querySelectorAll('.file-checkbox:checked');
const count = checkboxes.length;
const bulkBar = document.getElementById('bulkActionsBar');
const countSpan = document.getElementById('selectedFilesCount');
if (count > 0) {
bulkBar.style.display = 'block';
countSpan.textContent = count;
} else {
bulkBar.style.display = 'none';
document.getElementById('selectAllFiles').checked = false;
}
}
// Get selected file IDs
function getSelectedFileIds() {
const checkboxes = document.querySelectorAll('.file-checkbox:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.value));
}
// Bulk create invoices from selected files
async function bulkCreateInvoices() {
const fileIds = getSelectedFileIds();
if (fileIds.length === 0) {
alert('Vælg venligst filer først');
return;
}
if (!confirm(`Opret fakturaer fra ${fileIds.length} valgte filer?`)) return;
try {
let successCount = 0;
let failCount = 0;
for (const fileId of fileIds) {
try {
// Get extracted data
const dataResp = await fetch(`/api/v1/supplier-invoices/files/${fileId}/extracted-data`);
const data = await dataResp.json();
if (!data.llm_data || !data.vendor_matched_id) {
failCount++;
continue;
}
const llm = data.llm_data;
// Create invoice
const invoiceResp = await fetch('/api/v1/supplier-invoices', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
vendor_id: data.vendor_matched_id,
invoice_number: llm.invoice_number,
invoice_date: llm.invoice_date,
due_date: llm.due_date,
total_amount: llm.total_amount,
currency: llm.currency || 'DKK',
invoice_type: llm.document_type === 'credit_note' ? 'credit_note' : 'invoice',
status: 'unpaid',
notes: `Oprettet fra fil ID ${fileId}`,
lines: (llm.lines || []).map((line, idx) => ({
line_number: idx + 1,
sku: line.sku || null,
description: line.description,
quantity: line.quantity || 1,
unit_price: line.unit_price || 0,
line_total: line.line_total || 0,
vat_code: line.vat_note === 'reverse_charge' ? 'I52' : 'I25',
vat_rate: line.vat_rate || 25,
contra_account: '5810'
}))
})
});
if (invoiceResp.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
console.error(`Failed to create invoice from file ${fileId}:`, error);
failCount++;
}
}
alert(`${successCount} fakturaer oprettet\n${failCount > 0 ? `${failCount} fejlede` : ''}`);
// Reload data
loadPendingFiles();
loadInvoices();
loadStats();
} catch (error) {
console.error('Bulk create failed:', error);
alert('❌ Fejl ved bulk oprettelse: ' + error.message);
}
}
// Bulk reprocess selected files
async function bulkReprocess() {
const fileIds = getSelectedFileIds();
if (fileIds.length === 0) {
alert('Vælg venligst filer først');
return;
}
if (!confirm(`Genbehandle ${fileIds.length} valgte filer?`)) return;
try {
let successCount = 0;
let failCount = 0;
for (const fileId of fileIds) {
try {
const response = await fetch(`/api/v1/supplier-invoices/reprocess/${fileId}`, {
method: 'POST'
});
if (response.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
console.error(`Failed to reprocess file ${fileId}:`, error);
failCount++;
}
}
alert(`${successCount} filer genbehandlet\n${failCount > 0 ? `${failCount} fejlede` : ''}`);
loadPendingFiles();
} catch (error) {
console.error('Bulk reprocess failed:', error);
alert('❌ Fejl ved bulk genbehandling: ' + error.message);
}
}
// Bulk delete selected files
async function bulkDelete() {
const fileIds = getSelectedFileIds();
if (fileIds.length === 0) {
alert('Vælg venligst filer først');
return;
}
if (!confirm(`⚠️ ADVARSEL: Slet ${fileIds.length} valgte filer permanent?\n\nDenne handling kan ikke fortrydes!`)) return;
try {
let successCount = 0;
let failCount = 0;
for (const fileId of fileIds) {
try {
const response = await fetch(`/api/v1/supplier-invoices/files/${fileId}`, {
method: 'DELETE'
});
if (response.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
console.error(`Failed to delete file ${fileId}:`, error);
failCount++;
}
}
alert(`${successCount} filer slettet\n${failCount > 0 ? `${failCount} fejlede` : ''}`);
loadPendingFiles();
} catch (error) {
console.error('Bulk delete failed:', error);
alert('❌ Fejl ved bulk sletning: ' + error.message);
}
}
// ========== END BULK ACTIONS ==========
// ========== INVOICE BULK ACTIONS ==========
// Toggle select all invoices
function toggleSelectAllInvoices() {
const selectAll = document.getElementById('selectAllInvoices');
const checkboxes = document.querySelectorAll('.invoice-checkbox:not(:disabled)');
checkboxes.forEach(cb => cb.checked = selectAll.checked);
updateInvoiceBulkActionsBar();
}
// Update invoice bulk actions bar visibility and count
function updateInvoiceBulkActionsBar() {
const checkboxes = document.querySelectorAll('.invoice-checkbox:checked');
const count = checkboxes.length;
const bulkBar = document.getElementById('invoiceBulkActionsBar');
const countSpan = document.getElementById('selectedInvoicesCount');
if (count > 0) {
bulkBar.style.display = 'block';
countSpan.textContent = count;
} else {
bulkBar.style.display = 'none';
document.getElementById('selectAllInvoices').checked = false;
}
}
// Get selected invoice IDs
function getSelectedInvoiceIds() {
const checkboxes = document.querySelectorAll('.invoice-checkbox:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.dataset.invoiceId));
}
// Bulk send to e-conomic
async function bulkSendToEconomic() {
const invoiceIds = getSelectedInvoiceIds();
if (invoiceIds.length === 0) {
alert('Vælg venligst fakturaer først');
return;
}
if (!confirm(`Send ${invoiceIds.length} fakturaer til e-conomic kassekladde?`)) return;
try {
let successCount = 0;
let failCount = 0;
let errors = [];
for (const invoiceId of invoiceIds) {
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/send-to-economic`, {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
if (response.ok) {
successCount++;
} else {
const errorData = await response.json();
errors.push(`Faktura #${invoiceId}: ${errorData.detail || response.statusText}`);
failCount++;
}
} catch (error) {
console.error(`Failed to send invoice ${invoiceId}:`, error);
errors.push(`Faktura #${invoiceId}: ${error.message}`);
failCount++;
}
}
let message = `${successCount} fakturaer sendt til e-conomic`;
if (failCount > 0) {
message += `\n\n${failCount} fejlede:\n${errors.join('\n')}`;
}
alert(message);
// Reload invoices
loadInvoices(currentFilter);
} catch (error) {
console.error('Bulk send to e-conomic failed:', error);
alert('❌ Fejl ved bulk sending: ' + error.message);
}
}
// Bulk reset invoices (move back to pending)
async function bulkResetInvoices() {
const invoiceIds = getSelectedInvoiceIds();
if (invoiceIds.length === 0) {
alert('Vælg venligst fakturaer først');
return;
}
if (!confirm(`Nulstil ${invoiceIds.length} fakturaer til afventer behandling?`)) return;
try {
let successCount = 0;
let failCount = 0;
for (const invoiceId of invoiceIds) {
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ status: 'pending' })
});
if (response.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
console.error(`Failed to reset invoice ${invoiceId}:`, error);
failCount++;
}
}
alert(`${successCount} fakturaer nulstillet\n${failCount > 0 ? `${failCount} fejlede` : ''}`);
// Reload invoices
loadInvoices(currentFilter);
} catch (error) {
console.error('Bulk reset failed:', error);
alert('❌ Fejl ved bulk nulstilling: ' + error.message);
}
}
// Bulk mark as paid
async function bulkMarkAsPaid() {
const invoiceIds = getSelectedInvoiceIds();
if (invoiceIds.length === 0) {
alert('Vælg venligst fakturaer først');
return;
}
if (!confirm(`Marker ${invoiceIds.length} fakturaer som betalt?`)) return;
try {
let successCount = 0;
let failCount = 0;
for (const invoiceId of invoiceIds) {
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/mark-paid`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
paid_date: new Date().toISOString().split('T')[0]
})
});
if (response.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
console.error(`Failed to mark invoice ${invoiceId} as paid:`, error);
failCount++;
}
}
alert(`${successCount} fakturaer markeret som betalt\n${failCount > 0 ? `${failCount} fejlede` : ''}`);
// Reload invoices
loadInvoices(currentFilter);
} catch (error) {
console.error('Bulk mark as paid failed:', error);
alert('❌ Fejl ved bulk betaling: ' + error.message);
}
}
// Individual mark as paid
async function markInvoiceAsPaid(invoiceId) {
if (!confirm('Marker denne faktura som betalt?')) return;
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/mark-paid`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
paid_date: new Date().toISOString().split('T')[0]
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to mark as paid');
}
alert('✅ Faktura markeret som betalt');
loadInvoices(currentFilter);
} catch (error) {
console.error('Failed to mark invoice as paid:', error);
alert('❌ Fejl: ' + error.message);
}
}
// ========== END INVOICE BULK ACTIONS ==========
// ========== END PENDING FILES FUNCTIONS ==========
// Filter functions
function applyFilter(filter, element) {
currentFilter = filter;
// Update active pill
document.querySelectorAll('.filter-pill').forEach(pill => pill.classList.remove('active'));
element.classList.add('active');
// Reload invoices with filter
loadInvoices(filter);
}
// Open create modal
function openCreateModal() {
document.getElementById('invoiceForm').reset();
document.getElementById('invoiceId').value = '';
document.getElementById('lineItems').innerHTML = '';
setDefaultDates();
new bootstrap.Modal(document.getElementById('invoiceModal')).show();
}
// Add line item
let lineCounter = 0;
function addLine() {
lineCounter++;
const lineHtml = `
<div class="line-item" id="line-${lineCounter}">
<div class="row">
<div class="col-md-5">
<input type="text" class="form-control form-control-sm mb-2"
placeholder="Beskrivelse" name="line_description[]">
</div>
<div class="col-md-2">
<input type="number" step="1" class="form-control form-control-sm mb-2"
placeholder="Antal" name="line_quantity[]" value="1">
</div>
<div class="col-md-2">
<input type="number" step="0.01" class="form-control form-control-sm mb-2"
placeholder="Pris" name="line_price[]">
</div>
<div class="col-md-2">
<select class="form-select form-select-sm mb-2" name="line_vat_code[]">
<option value="I25">Moms 25%</option>
<option value="I0">Moms 0%</option>
<option value="IY25">Omvendt 25%</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeLine(${lineCounter})">
<i class="bi bi-x"></i>
</button>
</div>
</div>
</div>
`;
document.getElementById('lineItems').insertAdjacentHTML('beforeend', lineHtml);
}
function removeLine(lineId) {
document.getElementById(`line-${lineId}`)?.remove();
}
// Save invoice
async function saveInvoice() {
try {
const form = document.getElementById('invoiceForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
// Collect line items
const lines = [];
const descriptions = form.querySelectorAll('[name="line_description[]"]');
const quantities = form.querySelectorAll('[name="line_quantity[]"]');
const prices = form.querySelectorAll('[name="line_price[]"]');
const vatCodes = form.querySelectorAll('[name="line_vat_code[]"]');
for (let i = 0; i < descriptions.length; i++) {
if (descriptions[i].value.trim()) {
const qty = parseFloat(quantities[i].value) || 1;
const price = parseFloat(prices[i].value) || 0;
const lineTotal = qty * price;
// Calculate VAT based on code
let vatRate = 25;
if (vatCodes[i].value === 'I0') vatRate = 0;
const vatAmount = lineTotal * (vatRate / 100);
lines.push({
description: descriptions[i].value,
quantity: qty,
unit_price: price,
line_total: lineTotal + vatAmount,
vat_code: vatCodes[i].value,
vat_rate: vatRate,
vat_amount: vatAmount,
contra_account: '5810'
});
}
}
const data = {
invoice_number: document.getElementById('invoiceNumber').value,
vendor_id: parseInt(document.getElementById('vendorId').value),
invoice_date: document.getElementById('invoiceDate').value,
due_date: document.getElementById('dueDate').value,
total_amount: parseFloat(document.getElementById('totalAmount').value),
vat_amount: parseFloat(document.getElementById('vatAmount').value) || 0,
currency: document.getElementById('currency').value,
description: document.getElementById('description').value,
notes: document.getElementById('notes').value,
lines: lines
};
const response = await fetch('/api/v1/supplier-invoices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('invoiceModal')).hide();
loadInvoices(currentFilter);
loadStats();
alert('✅ Faktura oprettet');
} else {
const error = await response.json();
alert('❌ Fejl: ' + (error.detail || 'Kunne ikke oprette faktura'));
}
} catch (error) {
console.error('Failed to save invoice:', error);
alert('❌ Fejl ved oprettelse af faktura');
}
}
// View invoice details
async function viewInvoice(invoiceId) {
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}`);
const invoice = await response.json();
currentInvoiceId = invoiceId;
// Check if invoice can be edited (not yet sent to e-conomic)
const isEditable = !invoice.economic_voucher_number;
// Pre-load learning suggestions for each line
const lineSuggestions = {};
if (isEditable && invoice.lines && invoice.lines.length > 0) {
for (const line of invoice.lines) {
if (line.description && !line.vat_code) {
try {
const suggestionResp = await fetch(
`/api/v1/supplier-invoices/suggest-line-codes?vendor_id=${invoice.vendor_id}&description=${encodeURIComponent(line.description)}`
);
if (suggestionResp.ok) {
const suggestionData = await suggestionResp.json();
if (suggestionData.has_suggestions) {
lineSuggestions[line.id || line.description] = suggestionData.top_suggestion;
}
}
} catch (err) {
console.warn('Failed to fetch suggestion for line:', err);
}
}
}
}
const detailsHtml = `
<!-- Header Section -->
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted small mb-1">Fakturanummer</label>
${isEditable ?
`<input type="text" class="form-control" id="editInvoiceNumber" value="${invoice.invoice_number}">` :
`<div class="fw-bold">${invoice.invoice_number}</div>`
}
</div>
<div class="mb-3">
<label class="form-label text-muted small mb-1">Leverandør</label>
<div class="fw-bold">${invoice.vendor_full_name || invoice.vendor_name}</div>
</div>
<div class="mb-3">
<label class="form-label text-muted small mb-1">Status</label>
<div>${getStatusBadge(invoice.status)}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted small mb-1">Fakturadato</label>
${isEditable ?
`<input type="date" class="form-control" id="editInvoiceDate" value="${invoice.invoice_date}">` :
`<div class="fw-bold">${formatDate(invoice.invoice_date)}</div>`
}
</div>
<div class="mb-3">
<label class="form-label text-muted small mb-1">Forfaldsdato</label>
${isEditable ?
`<input type="date" class="form-control" id="editDueDate" value="${invoice.due_date}">` :
`<div class="fw-bold">${formatDate(invoice.due_date)}</div>`
}
</div>
<div class="mb-3">
<label class="form-label text-muted small mb-1">Totalbeløb</label>
${isEditable ?
`<div class="input-group">
<input type="number" step="0.01" class="form-control" id="editTotalAmount" value="${invoice.total_amount}">
<span class="input-group-text">kr.</span>
</div>` :
`<div class="fw-bold fs-5 text-success">${formatCurrency(invoice.total_amount)}</div>`
}
</div>
</div>
</div>
${invoice.notes ? `
<div class="mt-3 pt-3 border-top">
<label class="form-label text-muted small mb-1">Noter</label>
<div class="alert alert-light mb-0">${invoice.notes}</div>
</div>
` : ''}
${invoice.economic_voucher_number ? `
<div class="mt-3 pt-3 border-top">
<div class="alert alert-success mb-0">
<i class="bi bi-check-circle me-2"></i>
<strong>Sendt til e-conomic</strong> - Bilagsnummer: <strong>${invoice.economic_voucher_number}</strong>
<br><small>Kassekladde ${invoice.economic_journal_number}, Regnskabsår ${invoice.economic_accounting_year}</small>
</div>
</div>
` : ''}
</div>
</div>
<!-- Line Items Section -->
<div class="card">
<div class="card-header bg-light">
<h6 class="mb-0"><i class="bi bi-list-ul me-2"></i>Produktlinier (${(invoice.lines || []).length})</h6>
</div>
<div class="card-body p-0">
${isEditable ? `
<div class="alert alert-info mb-0 border-0 rounded-0" style="font-size: 0.875rem;">
💡 <strong>Momskoder:</strong>
<span class="badge bg-success ms-2">I25</span> 25% moms (standard) ·
<span class="badge bg-warning text-dark">I52</span> Omvendt betalingspligt ·
<span class="badge bg-secondary">I0</span> 0% (momsfri)
${Object.keys(lineSuggestions).length > 0 ? '<br>🤖 <strong>Smart forslag aktiveret</strong> - Grønne felter er auto-udfyldt baseret på historik' : ''}
</div>
` : ''}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 100px;">Varenr</th>
<th>Beskrivelse</th>
<th style="width: 80px;" class="text-end">Antal</th>
<th style="width: 120px;" class="text-end">Enhedspris</th>
<th style="width: 120px;" class="text-end">Total</th>
<th style="width: 140px;">Momskode</th>
<th style="width: 100px;">Modkonto</th>
</tr>
</thead>
<tbody id="invoiceLinesList">
${(invoice.lines || []).map((line, idx) => {
const lineKey = line.id || line.description;
const suggestion = lineSuggestions[lineKey];
const hasVat = !!line.vat_code;
const hasContra = !!line.contra_account;
const suggestedVat = suggestion?.vat_code || line.vat_code || 'I25';
const suggestedContra = suggestion?.contra_account || line.contra_account || '5810';
const suggestionTooltip = suggestion ?
`title="Smart forslag: Baseret på ${suggestion.match_count} tidligere matches (senest: ${suggestion.last_used})"` : '';
return `
<tr data-line-id="${line.id || idx}">
<td>
${isEditable ?
`<input type="text" class="form-control form-control-sm line-sku" value="${line.sku || ''}" placeholder="SKU">` :
`<code class="small">${line.sku || '-'}</code>`
}
</td>
<td>
${isEditable ?
`<input type="text" class="form-control form-control-sm line-description" value="${line.description || ''}" placeholder="Beskrivelse">` :
(line.description || '')
}
</td>
<td class="text-end">
${isEditable ?
`<input type="number" step="1" class="form-control form-control-sm text-end line-quantity" value="${line.quantity}">` :
line.quantity
}
</td>
<td class="text-end">
${isEditable ?
`<input type="number" step="0.01" class="form-control form-control-sm text-end line-price" value="${line.unit_price}">` :
formatCurrency(line.unit_price)
}
</td>
<td class="text-end"><strong>${formatCurrency(line.line_total)}</strong></td>
<td>
${isEditable ? `
<select class="form-select form-select-sm line-vat-code ${!hasVat && suggestion ? 'border-success bg-success bg-opacity-10' : ''}"
${suggestionTooltip}>
<option value="I25" ${suggestedVat === 'I25' ? 'selected' : ''}>I25 - 25%</option>
<option value="I52" ${suggestedVat === 'I52' ? 'selected' : ''}>I52 - Omvendt</option>
<option value="I0" ${suggestedVat === 'I0' ? 'selected' : ''}>I0 - Momsfri</option>
<option value="IY25" ${suggestedVat === 'IY25' ? 'selected' : ''}>IY25 - Ydelser 25%</option>
<option value="IYEU" ${suggestedVat === 'IYEU' ? 'selected' : ''}>IYEU - EU ydelser</option>
</select>
` : `<span class="badge bg-${line.vat_code === 'I25' ? 'success' : line.vat_code === 'I52' ? 'warning text-dark' : 'secondary'}">${line.vat_code}</span> <small class="text-muted">(${line.vat_rate}%)</small>`}
</td>
<td>
${isEditable ?
`<input type="text" class="form-control form-control-sm line-contra ${!hasContra && suggestion ? 'border-success bg-success bg-opacity-10' : ''}"
value="${suggestedContra}" ${suggestionTooltip}>` :
`<code class="small">${line.contra_account || '5810'}</code>`
}
</td>
</tr>
`}).join('')}
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="4" class="text-end"><strong>I alt:</strong></td>
<td class="text-end"><strong class="fs-5 text-success">${formatCurrency(invoice.total_amount)}</strong></td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
</div>
</div>
${isEditable ? `
<div class="card-footer bg-light">
<button type="button" class="btn btn-primary" onclick="saveInvoiceChanges()">
<i class="bi bi-save me-1"></i>Gem alle ændringer
</button>
</div>
` : ''}
</div>
`;
document.getElementById('invoiceDetails').innerHTML = detailsHtml;
// Show/hide action buttons based on status
document.getElementById('approveBtn').style.display = invoice.status === 'pending' ? 'inline-block' : 'none';
document.getElementById('sendToEconomicBtn').style.display =
(invoice.status === 'approved' && !invoice.economic_voucher_number) ? 'inline-block' : 'none';
new bootstrap.Modal(document.getElementById('viewInvoiceModal')).show();
} catch (error) {
console.error('Failed to load invoice:', error);
alert('❌ Kunne ikke hente faktura');
}
}
// Save invoice changes
async function saveInvoiceChanges() {
if (!currentInvoiceId) return;
try {
// Collect header data
const invoiceNumber = document.getElementById('editInvoiceNumber')?.value;
const invoiceDate = document.getElementById('editInvoiceDate')?.value;
const dueDate = document.getElementById('editDueDate')?.value;
const totalAmount = parseFloat(document.getElementById('editTotalAmount')?.value);
// Collect line items
const lineRows = document.querySelectorAll('#invoiceLinesList tr');
const lines = [];
lineRows.forEach((row, idx) => {
const sku = row.querySelector('.line-sku')?.value || '';
const description = row.querySelector('.line-description')?.value || '';
const quantity = parseFloat(row.querySelector('.line-quantity')?.value) || 1;
const unitPrice = parseFloat(row.querySelector('.line-price')?.value) || 0;
const vatCode = row.querySelector('.line-vat-code')?.value || 'I25';
const contraAccount = row.querySelector('.line-contra')?.value || '5810';
// Determine VAT rate from code
let vatRate = 25.00;
if (vatCode === 'I52' || vatCode === 'I0') {
vatRate = 0.00;
}
lines.push({
line_number: idx + 1,
sku: sku.trim() || null,
description: description,
quantity: quantity,
unit_price: unitPrice,
line_total: quantity * unitPrice,
vat_code: vatCode,
vat_rate: vatRate,
contra_account: contraAccount
});
});
// Update invoice
const response = await fetch(`/api/v1/supplier-invoices/${currentInvoiceId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
invoice_number: invoiceNumber,
invoice_date: invoiceDate,
due_date: dueDate,
total_amount: totalAmount,
lines: lines
})
});
if (response.ok) {
alert('✅ Ændringer gemt');
// Reload invoice details
await viewInvoice(currentInvoiceId);
// Refresh list
loadInvoices(currentFilter);
} else {
const error = await response.json();
alert('❌ Kunne ikke gemme: ' + (error.detail || 'Ukendt fejl'));
}
} catch (error) {
console.error('Failed to save invoice changes:', error);
alert('❌ Fejl ved gem: ' + error.message);
}
}
// Approve invoice
async function approveInvoice() {
if (!currentInvoiceId) return;
if (!confirm('Godkend denne faktura?')) return;
try {
const response = await fetch(`/api/v1/supplier-invoices/${currentInvoiceId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ approved_by: getApprovalUser() })
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('viewInvoiceModal')).hide();
loadInvoices(currentFilter);
loadStats();
alert('✅ Faktura godkendt');
} else {
const error = await response.json();
alert('❌ Fejl: ' + (error.detail || 'Kunne ikke godkende'));
}
} catch (error) {
console.error('Failed to approve:', error);
alert('❌ Fejl ved godkendelse');
}
}
// Send to e-conomic
async function sendToEconomic() {
if (!currentInvoiceId) return;
if (!confirm('Send denne faktura til e-conomic kassekladde?')) return;
try {
const response = await fetch(`/api/v1/supplier-invoices/${currentInvoiceId}/send-to-economic`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
bootstrap.Modal.getInstance(document.getElementById('viewInvoiceModal')).hide();
loadInvoices(currentFilter);
loadStats();
alert(`✅ Faktura sendt til e-conomic\nBilagsnummer: ${result.voucher_number}`);
} else {
const error = await response.json();
alert('❌ Fejl: ' + (error.detail || 'Kunne ikke sende til e-conomic'));
}
} catch (error) {
console.error('Failed to send to e-conomic:', error);
alert('❌ Fejl ved sending til e-conomic');
}
}
// Quick approve
async function quickApprove(invoiceId) {
if (!confirm('Godkend denne faktura?')) return;
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ approved_by: getApprovalUser() })
});
if (response.ok) {
loadInvoices(currentFilter);
loadStats();
} else {
alert('❌ Kunne ikke godkende');
}
} catch (error) {
console.error('Failed to approve:', error);
}
}
// Quick send to e-conomic
async function quickSendToEconomic(invoiceId) {
if (!confirm('Send til e-conomic?')) return;
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/send-to-economic`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
loadInvoices(currentFilter);
loadStats();
alert(`✅ Sendt - Bilag #${result.voucher_number}`);
} else {
const error = await response.json();
alert('❌ Fejl: ' + (error.detail || 'Kunne ikke sende'));
}
} catch (error) {
console.error('Failed to send:', error);
}
}
// Delete invoice
async function deleteInvoice(invoiceId) {
if (!confirm('Slet denne faktura?')) return;
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}`, {
method: 'DELETE'
});
if (response.ok) {
loadInvoices(currentFilter);
loadStats();
} else {
alert('❌ Kunne ikke slette');
}
} catch (error) {
console.error('Failed to delete:', error);
}
}
// Utility functions
function formatCurrency(amount) {
if (amount === null || amount === undefined) return '-';
return new Intl.NumberFormat('da-DK', {
style: 'currency',
currency: 'DKK'
}).format(amount);
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString('da-DK');
}
function getStatusBadge(status) {
const badges = {
'pending': '<span class="badge status-pending">Afventer</span>',
'approved': '<span class="badge status-approved">Godkendt</span>',
'sent_to_economic': '<span class="badge status-sent">Sendt</span>',
'paid': '<span class="badge status-paid">Betalt</span>',
'overdue': '<span class="badge status-overdue">Overskredet</span>',
'cancelled': '<span class="badge bg-secondary">Annulleret</span>'
};
return badges[status] || `<span class="badge bg-secondary">${status}</span>`;
}
// ========== UPLOAD FUNCTIONS ==========
function openUploadModal() {
document.getElementById('uploadForm').reset();
document.getElementById('uploadProgress').classList.add('d-none');
document.getElementById('uploadResult').classList.add('d-none');
document.getElementById('uploadResult').innerHTML = '';
document.getElementById('uploadBtn').disabled = false;
document.getElementById('fileCount').textContent = '';
// Add event listener for file selection
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', function() {
const count = this.files.length;
const fileCountDiv = document.getElementById('fileCount');
if (count > 0) {
fileCountDiv.innerHTML = `<i class="bi bi-files me-1"></i><strong>${count} fil(er) valgt</strong>`;
} else {
fileCountDiv.textContent = '';
}
});
new bootstrap.Modal(document.getElementById('uploadModal')).show();
}
async function uploadInvoice() {
const fileInput = document.getElementById('fileInput');
const files = Array.from(fileInput.files);
if (files.length === 0) {
alert('Vælg venligst minimum én fil');
return;
}
// Validate all files
const allowedTypes = ['.pdf', '.png', '.jpg', '.jpeg'];
for (const file of files) {
const ext = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedTypes.includes(ext)) {
alert(`Kun PDF, PNG eller JPG filer tilladt: ${file.name}`);
return;
}
// Validate file size (50 MB)
if (file.size > 50 * 1024 * 1024) {
alert(`Fil for stor (max 50 MB): ${file.name}`);
return;
}
}
const uploadBtn = document.getElementById('uploadBtn');
const progressDiv = document.getElementById('uploadProgress');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const resultDiv = document.getElementById('uploadResult');
const results = [];
const totalFiles = files.length;
try {
// Show progress
uploadBtn.disabled = true;
progressDiv.classList.remove('d-none');
resultDiv.classList.add('d-none');
// Upload each file
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileNum = i + 1;
progressBar.style.width = `${(i / totalFiles) * 100}%`;
progressText.textContent = `Uploader fil ${fileNum}/${totalFiles}: ${file.name}...`;
// Create FormData
const formData = new FormData();
formData.append('file', file);
try {
// Upload file
const response = await fetch('/api/v1/supplier-invoices/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
results.push({ file: file.name, success: response.ok, result });
} catch (error) {
results.push({ file: file.name, success: false, result: { message: error.message } });
}
}
progressBar.style.width = '100%';
progressText.textContent = `Færdig! ${totalFiles} fil(er) uploadet`;
// Hide progress after animation
setTimeout(() => {
progressDiv.classList.add('d-none');
showMultiUploadResult(results);
}, 500);
} catch (error) {
console.error('Upload failed:', error);
progressDiv.classList.add('d-none');
showMultiUploadResult([{ file: 'Unknown', success: false, result: { message: error.message } }]);
} finally {
uploadBtn.disabled = false;
}
}
// Show results for multiple file uploads
function showMultiUploadResult(results) {
const resultDiv = document.getElementById('uploadResult');
resultDiv.classList.remove('d-none');
const successCount = results.filter(r => r.success).length;
const failCount = results.length - successCount;
let html = '';
if (successCount > 0) {
html += `
<div class="alert alert-success">
<h6><i class="bi bi-check-circle me-2"></i>${successCount} Faktura(er) Uploadet!</h6>
<p class="mb-2">Filerne er uploadet og klar til gennemgang.</p>
<ul class="small mb-0">
`;
results.filter(r => r.success).forEach(r => {
html += `<li><strong>${r.file}</strong> - ${r.result.message || 'Uploadet'}</li>`;
});
html += `</ul></div>`;
}
if (failCount > 0) {
html += `
<div class="alert alert-warning mt-2">
<h6><i class="bi bi-exclamation-triangle me-2"></i>${failCount} Fil(er) Fejlede</h6>
<ul class="small mb-0">
`;
results.filter(r => !r.success).forEach(r => {
html += `<li><strong>${r.file}</strong> - ${r.result.message || 'Ukendt fejl'}</li>`;
});
html += `</ul></div>`;
}
html += `
<div class="mt-3">
<button class="btn btn-primary btn-sm" onclick="closeUploadAndRefresh()">
<i class="bi bi-check-lg me-1"></i>OK - Gå til Mangler Behandling
</button>
</div>
`;
resultDiv.innerHTML = html;
}
function showUploadResult(result, success) {
const resultDiv = document.getElementById('uploadResult');
resultDiv.classList.remove('d-none');
if (result.status === 'duplicate') {
// Duplicate file or invoice
resultDiv.innerHTML = `
<div class="alert alert-warning">
<h6><i class="bi bi-exclamation-triangle me-2"></i>Duplicate Detected</h6>
<p class="mb-2">${result.message}</p>
${result.invoice_id ? `
<button class="btn btn-sm btn-outline-primary" onclick="viewInvoice(${result.invoice_id})">
<i class="bi bi-eye me-1"></i>Vis eksisterende faktura
</button>
` : ''}
</div>
`;
} else if (success && (result.status === 'success' || result.status === 'needs_review')) {
// Success - show that file is ready for review
resultDiv.innerHTML = `
<div class="alert alert-success">
<h6><i class="bi bi-check-circle me-2"></i>Faktura Uploadet!</h6>
<p class="mb-3">${result.message || 'Filen er uploadet og klar til gennemgang.'}</p>
<div class="mt-3">
<button class="btn btn-primary btn-sm" onclick="closeUploadAndRefresh()">
<i class="bi bi-check-lg me-1"></i>OK - Gå til Mangler Behandling
</button>
</div>
</div>
`;
// Auto-refresh file list
setTimeout(() => {
loadPendingFiles();
loadStats();
}, 500);
} else if (result.status === 'error' && result.needs_review) {
// AI parsing failed - needs manual review
resultDiv.innerHTML = `
<div class="alert alert-warning">
<h6><i class="bi bi-exclamation-triangle me-2"></i>AI-Analyse Fejlede - Manuel Gennemgang Nødvendig</h6>
<p class="mb-2">${result.message}</p>
${result.error_details ? `
<details class="mt-2">
<summary class="text-muted small">Tekniske detaljer</summary>
<pre class="small mt-2 text-muted">${result.error_details}</pre>
</details>
` : ''}
<div class="mt-3">
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
Filen er gemt og kan behandles manuelt senere.
</small>
</div>
<button class="btn btn-secondary btn-sm mt-2" onclick="closeUploadAndRefresh()">
<i class="bi bi-check-lg me-1"></i>OK
</button>
</div>
`;
} else {
// Error
resultDiv.innerHTML = `
<div class="alert alert-danger">
<h6><i class="bi bi-x-circle me-2"></i>Upload Fejlede</h6>
<p class="mb-0">${result.message || result.detail || 'Ukendt fejl'}</p>
</div>
`;
}
}
function closeUploadAndRefresh() {
bootstrap.Modal.getInstance(document.getElementById('uploadModal')).hide();
loadInvoices(currentFilter);
loadStats();
}
// Create template from successfully uploaded invoice
async function createTemplateFromInvoice(invoiceId, vendorId) {
try {
const loadingDiv = document.getElementById('uploadResult');
loadingDiv.innerHTML = `
<div class="alert alert-info">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-3" role="status"></div>
<div>
<strong>Opretter template...</strong><br>
<small>AI analyserer faktura og genererer patterns...</small>
</div>
</div>
</div>
`;
// Step 1: Get PDF text from invoice
const reprocessResp = await fetch(`/api/v1/supplier-invoices/reprocess/${invoiceId}`, {
method: 'POST'
});
const pdfData = await reprocessResp.json();
if (!pdfData.pdf_text) {
throw new Error('Kunne ikke hente PDF tekst');
}
// Step 2: AI analyze
const aiResp = await fetch('/api/v1/supplier-invoices/ai/analyze', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
pdf_text: pdfData.pdf_text,
vendor_id: vendorId
})
});
const aiData = await aiResp.json();
// Step 3: Convert AI data to template format
const fieldMappings = {};
const detectionPatterns = [];
// Build field mappings from AI response
if (aiData.vendor_cvr || aiData.cvr) {
const cvrValue = aiData.vendor_cvr?.value || aiData.vendor_cvr || aiData.cvr;
fieldMappings.vendor_cvr = {
pattern: aiData.vendor_cvr?.pattern || `DK\\s*(${cvrValue})`,
group: 1
};
}
if (aiData.invoice_number) {
const invNum = aiData.invoice_number?.value || aiData.invoice_number;
fieldMappings.invoice_number = {
pattern: aiData.invoice_number?.pattern || `(?:Nummer|Faktura|Invoice)\\s*(${invNum})`,
group: 1
};
}
if (aiData.invoice_date) {
fieldMappings.invoice_date = {
pattern: aiData.invoice_date?.pattern || `Dato\\s*(\\d{1,2}[\\/.\\-]\\d{1,2}[\\/.\\-]\\d{2,4})`,
group: 1,
format: "DD.MM.YYYY"
};
}
if (aiData.total_amount) {
fieldMappings.total_amount = {
pattern: aiData.total_amount?.pattern || `(?:Total|I alt|Beløb)\\s*([\\d.,]+)`,
group: 1
};
}
// Lines markers
if (aiData.lines_start) {
fieldMappings.lines_start = {
pattern: aiData.lines_start?.pattern || aiData.lines_start
};
}
if (aiData.lines_end) {
fieldMappings.lines_end = {
pattern: aiData.lines_end?.pattern || aiData.lines_end
};
}
// Detection patterns
if (aiData.detection_patterns && Array.isArray(aiData.detection_patterns)) {
aiData.detection_patterns.forEach((p, i) => {
const pattern = typeof p === 'string' ? p : p.pattern;
const weight = typeof p === 'object' ? p.weight : (0.5 - i * 0.1);
detectionPatterns.push({
type: 'text',
pattern: pattern,
weight: Math.max(0.1, weight)
});
});
} else {
// Default detection patterns if none provided
detectionPatterns.push({type: 'text', pattern: 'Faktura', weight: 0.3});
}
// Step 4: Create template
const createResp = await fetch('/api/v1/supplier-invoices/templates', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
vendor_id: vendorId,
template_name: `Auto-generated ${new Date().toISOString().split('T')[0]}`,
detection_patterns: detectionPatterns,
field_mappings: fieldMappings
})
});
const template = await createResp.json();
if (!createResp.ok) {
throw new Error(template.detail || 'Kunne ikke oprette template');
}
// Success!
loadingDiv.innerHTML = `
<div class="alert alert-success">
<h6><i class="bi bi-check-circle me-2"></i>Template Oprettet!</h6>
<p class="mb-2">Næste gang en faktura fra denne leverandør uploades, vil den blive behandlet automatisk på 0.1 sekunder i stedet for ${aiData._processing_time || '10'} sekunder!</p>
<div class="mt-3">
<a href="/billing/template-builder?template_id=${template.template_id}" class="btn btn-primary btn-sm me-2">
<i class="bi bi-pencil me-1"></i>Rediger Template
</a>
<a href="/billing/templates" class="btn btn-outline-primary btn-sm me-2">
<i class="bi bi-list me-1"></i>Se Alle Templates
</a>
<button class="btn btn-success btn-sm" onclick="closeUploadAndRefresh()">
<i class="bi bi-check-lg me-1"></i>Luk
</button>
</div>
</div>
`;
} catch (error) {
console.error('Template creation error:', error);
document.getElementById('uploadResult').innerHTML = `
<div class="alert alert-danger">
<h6><i class="bi bi-x-circle me-2"></i>Kunne ikke oprette template</h6>
<p class="mb-0">${error.message}</p>
<button class="btn btn-secondary btn-sm mt-2" onclick="closeUploadAndRefresh()">Luk</button>
</div>
`;
}
}
// ========== NEW HELPER FUNCTIONS ==========
// Send single invoice to e-conomic from kassekladde
async function sendSingleToEconomic(invoiceId) {
if (!confirm('⚠️ ADVARSEL!\n\nSend denne faktura til e-conomic?\n\nDette vil oprette et kassekladde-bilag i e-conomic.\n\nForsæt?')) {
return;
}
try {
showLoadingOverlay('Sender til e-conomic...');
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/send-to-economic`, {
method: 'POST'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to send');
}
const result = await response.json();
hideLoadingOverlay();
alert(`✅ Faktura sendt til e-conomic!\n\nBilagsnummer: ${result.voucher_number || 'N/A'}`);
// Reload kassekladde
loadKassekladdeView();
} catch (error) {
hideLoadingOverlay();
console.error('Failed to send to e-conomic:', error);
alert('❌ Fejl ved afsendelse til e-conomic:\n\n' + error.message);
}
}
// Bulk send selected invoices to e-conomic
async function bulkSendToEconomicKassekladde() {
const checkboxes = document.querySelectorAll('.kassekladde-checkbox:checked');
const invoiceIds = Array.from(checkboxes).map(cb => parseInt(cb.dataset.invoiceId));
if (invoiceIds.length === 0) {
alert('Vælg mindst én faktura');
return;
}
if (!confirm(`⚠️ ADVARSEL!\n\nSend ${invoiceIds.length} faktura(er) til e-conomic?\n\nDette vil oprette kassekladde-bilager i e-conomic.\n\nForsæt?`)) {
return;
}
try {
showLoadingOverlay(`Sender ${invoiceIds.length} fakturaer...`);
let success = 0;
let failed = 0;
for (const invoiceId of invoiceIds) {
try {
const response = await fetch(`/api/v1/supplier-invoices/${invoiceId}/send-to-economic`, {
method: 'POST'
});
if (response.ok) {
success++;
} else {
failed++;
}
} catch (error) {
failed++;
}
}
hideLoadingOverlay();
alert(`✅ Bulk-afsendelse gennemført!\n\nSucces: ${success}\nFejlet: ${failed}`);
// Reload kassekladde
loadKassekladdeView();
} catch (error) {
hideLoadingOverlay();
console.error('Bulk send error:', error);
alert('❌ Fejl ved bulk-afsendelse');
}
}
function getApprovalUser() {
const bodyUser = document.body?.dataset?.currentUser;
if (bodyUser && bodyUser.trim()) return bodyUser.trim();
const metaUser = document.querySelector('meta[name="current-user"]')?.content;
if (metaUser && metaUser.trim()) return metaUser.trim();
return 'System';
}
// Select vendor for file (when <100% match)
async function selectVendorForFile(fileId, vendorId) {
if (!vendorId) return;
try {
showLoadingOverlay('Opdaterer leverandør...');
// Update file with selected vendor
const response = await fetch(`/api/v1/incoming-files/${fileId}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
detected_vendor_id: parseInt(vendorId)
})
});
if (!response.ok) throw new Error('Failed to update vendor');
hideLoadingOverlay();
// Re-analyze file with selected vendor
await analyzeFile(fileId);
} catch (error) {
hideLoadingOverlay();
console.error('Vendor selection error:', error);
alert('❌ Fejl ved valg af leverandør');
}
}
// Delete file
async function deleteFile(fileId) {
if (!confirm('Slet denne fil permanent?')) return;
try {
const response = await fetch(`/api/v1/incoming-files/${fileId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Delete failed');
alert('✅ Fil slettet');
loadUnhandledFiles();
} catch (error) {
console.error('Delete error:', error);
alert('❌ Fejl ved sletning');
}
}
// View PDF file
async function viewFilePDF(fileId) {
const pdfUrl = `/api/v1/supplier-invoices/files/${fileId}/pdf`;
window.open(pdfUrl, '_blank');
}
// Loading overlay functions
function showLoadingOverlay(message = 'Behandler...') {
let overlay = document.getElementById('loadingOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'loadingOverlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
`;
overlay.innerHTML = `
<div style="background: white; padding: 2rem; border-radius: 12px; text-align: center;">
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Loading...</span>
</div>
<div id="loadingMessage" style="font-size: 1.1rem; font-weight: 500;"></div>
</div>
`;
document.body.appendChild(overlay);
}
document.getElementById('loadingMessage').textContent = message;
overlay.style.display = 'flex';
}
function hideLoadingOverlay() {
const overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.style.display = 'none';
}
}
</script>
{% endblock %}