- Updated billing frontend views to use Jinja2 templates for rendering HTML pages. - Added support for displaying supplier invoices, template builder, and templates list with titles. - Introduced a new configuration setting for company CVR number. - Enhanced OllamaService to support credit notes in invoice extraction, including detailed JSON output format. - Improved PDF text extraction using pdfplumber for better layout handling. - Added a modal for editing vendor details with comprehensive fields and validation. - Implemented invoice loading and display functionality in vendor detail view. - Updated vendor management to remove priority handling and improve error messaging. - Added tests for AI analyze endpoint and CVR filtering to ensure correct behavior. - Created migration script to support credit notes in the database schema.
2433 lines
99 KiB
HTML
2433 lines
99 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Kassekladde - 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="invoices-tab" data-bs-toggle="tab" href="#invoices-content" onclick="switchToInvoicesTab()">
|
|
<i class="bi bi-receipt me-2"></i>Fakturaer
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="pending-files-tab" data-bs-toggle="tab" href="#pending-files-content" onclick="switchToPendingFilesTab()">
|
|
<i class="bi bi-cloud-upload me-2"></i>Uploadede Filer
|
|
<span class="badge bg-warning text-dark ms-2" id="pendingFilesCount" style="display: none;">0</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="tab-content" id="mainTabContent">
|
|
|
|
<!-- Invoices Tab -->
|
|
<div class="tab-pane fade show active" id="invoices-content">
|
|
|
|
<!-- Filters -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<div class="filter-pills">
|
|
<div class="filter-pill active" data-filter="all" onclick="applyFilter('all', this)">
|
|
<i class="bi bi-list-ul me-1"></i>Alle
|
|
</div>
|
|
<div class="filter-pill" data-filter="pending" onclick="applyFilter('pending', this)">
|
|
<i class="bi bi-clock me-1"></i>Afventer
|
|
</div>
|
|
<div class="filter-pill" data-filter="approved" onclick="applyFilter('approved', this)">
|
|
<i class="bi bi-check-circle me-1"></i>Godkendt
|
|
</div>
|
|
<div class="filter-pill" data-filter="sent_to_economic" onclick="applyFilter('sent_to_economic', this)">
|
|
<i class="bi bi-send me-1"></i>Sendt til e-conomic
|
|
</div>
|
|
<div class="filter-pill" data-filter="overdue" onclick="applyFilter('overdue', this)">
|
|
<i class="bi bi-exclamation-triangle me-1"></i>Overskredet
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Invoices Table -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Fakturanr.</th>
|
|
<th>Leverandør</th>
|
|
<th>Fakturadato</th>
|
|
<th>Forfaldsdato</th>
|
|
<th>Beløb</th>
|
|
<th>Status</th>
|
|
<th>e-conomic</th>
|
|
<th>Handlinger</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="invoicesTable">
|
|
<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>
|
|
|
|
<!-- 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">📁 Uploadede filer afventer behandling</h5>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="loadPendingFiles()">
|
|
<i class="bi bi-arrow-clockwise me-2"></i>Opdater
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Filnavn</th>
|
|
<th>Upload Dato</th>
|
|
<th>Status</th>
|
|
<th>Leverandør</th>
|
|
<th>Template</th>
|
|
<th>Handlinger</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="pendingFilesTable">
|
|
<tr>
|
|
<td colspan="5" 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-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Fakturadetaljer</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="invoiceDetails">
|
|
<!-- Details will be loaded dynamically -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<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 modal-xl">
|
|
<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 Viewer -->
|
|
<div class="col-md-6">
|
|
<h6 class="mb-3">PDF Dokument</h6>
|
|
<div style="border: 1px solid #ddd; border-radius: 4px; height: 600px; overflow: auto;">
|
|
<embed id="manualEntryPdfViewer" type="application/pdf" width="100%" height="100%">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: Form -->
|
|
<div class="col-md-6">
|
|
<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-6">
|
|
<label class="form-label">Fakturanummer *</label>
|
|
<input type="text" class="form-control" id="manualInvoiceNumber" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Type</label>
|
|
<select class="form-select" 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">Fakturadato *</label>
|
|
<input type="date" class="form-control" id="manualInvoiceDate" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Forfaldsdato</label>
|
|
<input type="date" class="form-control" id="manualDueDate">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Total Beløb *</label>
|
|
<input type="number" step="0.01" class="form-control" id="manualTotalAmount" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Valuta</label>
|
|
<select class="form-select" 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">Fakturalinjer</label>
|
|
<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>
|
|
|
|
<!-- 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 = [];
|
|
|
|
// Load data on page load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadStats();
|
|
loadInvoices();
|
|
loadVendors();
|
|
setDefaultDates();
|
|
loadPendingFilesCount(); // Load count for badge
|
|
});
|
|
|
|
// 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="8" 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';
|
|
return `
|
|
<tr onclick="viewInvoice(${inv.id})" style="cursor: pointer;" class="${isCreditNote ? 'table-warning' : ''}">
|
|
<td>
|
|
${isCreditNote ? '<i class="bi bi-arrow-return-left text-warning me-1" title="Kreditnota"></i>' : ''}
|
|
<strong>${inv.invoice_number}</strong>
|
|
</td>
|
|
<td>${inv.vendor_full_name || inv.vendor_name || '-'}</td>
|
|
<td>${formatDate(inv.invoice_date)}</td>
|
|
<td>${formatDate(inv.due_date)}</td>
|
|
<td><strong class="${isCreditNote ? 'text-danger' : ''}">${formatCurrency(inv.total_amount)}</strong></td>
|
|
<td>${getStatusBadge(inv.computed_status || inv.status)}</td>
|
|
<td>${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-danger" onclick="deleteInvoice(${inv.id})" title="Slet">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`}).join('');
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// ========== 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);
|
|
}
|
|
}
|
|
|
|
// Switch to invoices tab
|
|
function switchToInvoicesTab() {
|
|
// Load invoices when switching to this tab
|
|
loadInvoices(currentFilter);
|
|
}
|
|
|
|
// Switch to pending files tab
|
|
function switchToPendingFilesTab() {
|
|
// Load pending files when switching to this tab
|
|
loadPendingFiles();
|
|
}
|
|
|
|
// Load pending uploaded files
|
|
async function loadPendingFiles() {
|
|
try {
|
|
const tbody = document.getElementById('pendingFilesTable');
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="5" 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="5" 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="6" class="text-center text-muted py-4">
|
|
<i class="bi bi-inbox me-2"></i>
|
|
Ingen uploadede filer afventer behandling
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = files.map(file => `
|
|
<tr>
|
|
<td>
|
|
<i class="bi bi-file-earmark-pdf text-danger me-2"></i>
|
|
<strong>${file.filename}</strong>
|
|
</td>
|
|
<td>${formatDate(file.uploaded_at)}</td>
|
|
<td>${getFileStatusBadge(file.status)}</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">Template #${file.template_id}</span>` : '<span class="text-muted">Ingen</span>'}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
${file.status === 'ai_extracted' || file.status === 'processed' ? `
|
|
<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' ? `
|
|
<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' ? `
|
|
<button class="btn btn-outline-secondary" disabled>
|
|
<i class="bi bi-hourglass-split me-1"></i>Behandler...
|
|
</button>
|
|
` : ''}
|
|
<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>'
|
|
};
|
|
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();
|
|
|
|
// 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>
|
|
|
|
<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 ? `
|
|
<h6 class="mt-3">PDF Tekst Preview:</h6>
|
|
<div class="border rounded p-3 bg-light" style="max-height: 500px; overflow-y: auto;">
|
|
<pre class="mb-0" style="font-size: 0.85rem; white-space: pre-wrap; word-wrap: break-word;">${data.pdf_text_preview}</pre>
|
|
</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 success message and offer to review
|
|
if (confirm('Fil behandlet med succes!\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);
|
|
}
|
|
}
|
|
|
|
// 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 data = await response.json();
|
|
const vendors = data.vendors || [];
|
|
|
|
const resultsDiv = document.getElementById('vendorSearchResults');
|
|
|
|
if (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 currentQuery = document.getElementById('vendorSearchInput').value;
|
|
if (currentQuery) {
|
|
searchVendorsForLink(currentQuery);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Close review modal
|
|
const reviewModal = bootstrap.Modal.getInstance(document.getElementById('reviewModal'));
|
|
if (reviewModal) {
|
|
reviewModal.hide();
|
|
}
|
|
|
|
// Set file ID
|
|
document.getElementById('manualEntryFileId').value = fileId;
|
|
|
|
// Load PDF
|
|
console.log('Loading PDF...');
|
|
document.getElementById('manualEntryPdfViewer').src = `/api/v1/supplier-invoices/files/${fileId}/pdf`;
|
|
|
|
// Load vendors into dropdown
|
|
console.log('Loading vendors...');
|
|
await loadVendorsForManual();
|
|
|
|
// Clear form
|
|
document.getElementById('manualEntryForm').reset();
|
|
document.getElementById('manualLineItems').innerHTML = '';
|
|
manualLineCounter = 0;
|
|
|
|
// 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;
|
|
|
|
// Invoice number
|
|
if (llm.invoice_number) {
|
|
document.getElementById('manualInvoiceNumber').value = llm.invoice_number;
|
|
}
|
|
|
|
// 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('manualVendorSelect').value = data.vendor_matched_id;
|
|
}
|
|
|
|
// Add line items
|
|
if (llm.lines && llm.lines.length > 0) {
|
|
llm.lines.forEach(line => {
|
|
addManualLine();
|
|
const lineNum = manualLineCounter;
|
|
|
|
if (line.description) {
|
|
document.getElementById(`manualLineDesc${lineNum}`).value = line.description;
|
|
}
|
|
if (line.quantity) {
|
|
document.getElementById(`manualLineQty${lineNum}`).value = line.quantity;
|
|
}
|
|
if (line.unit_price) {
|
|
document.getElementById(`manualLinePrice${lineNum}`).value = Math.abs(line.unit_price);
|
|
}
|
|
if (line.vat_rate) {
|
|
document.getElementById(`manualLineVat${lineNum}`).value = line.vat_rate;
|
|
}
|
|
});
|
|
} 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();
|
|
}
|
|
|
|
// Open modal
|
|
console.log('Opening manual entry modal...');
|
|
const manualModal = new bootstrap.Modal(document.getElementById('manualEntryModal'));
|
|
manualModal.show();
|
|
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);
|
|
}
|
|
}
|
|
|
|
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-3">
|
|
<div class="row g-2">
|
|
<div class="col-md-5">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Beskrivelse"
|
|
id="manualLineDesc${manualLineCounter}" name="line_description[]">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<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">
|
|
<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">
|
|
<input type="number" class="form-control form-control-sm" placeholder="Moms %"
|
|
id="manualLineVat${manualLineCounter}" name="line_vat[]" value="25" step="0.01">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<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 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 descriptions = document.getElementsByName('line_description[]');
|
|
const quantities = document.getElementsByName('line_quantity[]');
|
|
const prices = document.getElementsByName('line_price[]');
|
|
|
|
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;
|
|
lines.push({
|
|
line_number: i + 1,
|
|
description: descriptions[i].value,
|
|
quantity: qty,
|
|
unit_price: price,
|
|
line_total: qty * price,
|
|
vat_rate: 25.00
|
|
});
|
|
}
|
|
}
|
|
|
|
// Create invoice
|
|
const 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: `Manuel indtastning fra fil ID ${fileId}. ${notes}`.trim(),
|
|
lines: lines
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
|
|
// Mark file as completed
|
|
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();
|
|
alert(`✅ ${invoiceType === 'credit_note' ? 'Kreditnota' : 'Faktura'} oprettet!\n\nFakturanummer: ${invoiceNumber}\nBeløb: ${totalAmount} ${currency}`);
|
|
|
|
loadPendingFiles();
|
|
loadInvoices();
|
|
loadStats();
|
|
} else {
|
|
const error = await response.json();
|
|
alert('Kunne ikke oprette 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 ==========
|
|
|
|
// ========== 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;
|
|
|
|
const detailsHtml = `
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<p class="mb-1"><strong>Fakturanummer:</strong> ${invoice.invoice_number}</p>
|
|
<p class="mb-1"><strong>Leverandør:</strong> ${invoice.vendor_full_name || invoice.vendor_name}</p>
|
|
<p class="mb-1"><strong>Status:</strong> ${getStatusBadge(invoice.status)}</p>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<p class="mb-1"><strong>Fakturadato:</strong> ${formatDate(invoice.invoice_date)}</p>
|
|
<p class="mb-1"><strong>Forfaldsdato:</strong> ${formatDate(invoice.due_date)}</p>
|
|
<p class="mb-1"><strong>Total:</strong> <strong>${formatCurrency(invoice.total_amount)}</strong></p>
|
|
</div>
|
|
</div>
|
|
|
|
${invoice.description ? `<p><strong>Beskrivelse:</strong> ${invoice.description}</p>` : ''}
|
|
${invoice.notes ? `<p><strong>Noter:</strong> ${invoice.notes}</p>` : ''}
|
|
|
|
${invoice.economic_voucher_number ? `
|
|
<div class="alert alert-success">
|
|
<i class="bi bi-check-circle me-2"></i>
|
|
Sendt til e-conomic - Bilagsnummer: <strong>${invoice.economic_voucher_number}</strong>
|
|
(Kassekladde ${invoice.economic_journal_number}, År ${invoice.economic_accounting_year})
|
|
</div>
|
|
` : ''}
|
|
|
|
<h6 class="mt-3">Linjer:</h6>
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Beskrivelse</th>
|
|
<th>Antal</th>
|
|
<th>Pris</th>
|
|
<th>Moms</th>
|
|
<th>Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${(invoice.lines || []).map(line => `
|
|
<tr>
|
|
<td>${line.description}</td>
|
|
<td>${line.quantity}</td>
|
|
<td>${formatCurrency(line.unit_price)}</td>
|
|
<td>${line.vat_code} (${line.vat_rate}%)</td>
|
|
<td><strong>${formatCurrency(line.line_total)}</strong></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
// 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: 'CurrentUser' }) // TODO: Get from auth
|
|
});
|
|
|
|
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: 'CurrentUser' })
|
|
});
|
|
|
|
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 Uploadede Filer
|
|
</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 Uploadede Filer
|
|
</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>
|
|
`;
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|