5280 lines
228 KiB
HTML
5280 lines
228 KiB
HTML
{% 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"> </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"> </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 %}
|