- Added `check_invoice_number_exists` method in `EconomicService` to verify invoice numbers in e-conomic journals. - Introduced `quick_analysis_on_upload` method in `OllamaService` for extracting critical fields from uploaded PDFs, including CVR, document type, and document number. - Created migration script to add new fields for storing detected CVR, vendor ID, document type, and document number in the `incoming_files` table. - Developed comprehensive tests for the quick analysis functionality, validating CVR detection, document type identification, and invoice number extraction.
1896 lines
85 KiB
HTML
1896 lines
85 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="da">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Template Builder - BMC Hub</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
|
<style>
|
|
:root {
|
|
--bg-body: #f8f9fa;
|
|
--bg-card: #ffffff;
|
|
--text-primary: #2c3e50;
|
|
--accent: #0f4c75;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-body);
|
|
color: var(--text-primary);
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
padding-top: 80px;
|
|
}
|
|
|
|
.pdf-preview {
|
|
background: #2c3e50;
|
|
color: #ecf0f1;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.pattern-test {
|
|
background: #fff3cd;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
margin-top: 10px;
|
|
border-left: 3px solid #ffc107;
|
|
}
|
|
|
|
.match-highlight {
|
|
background: #28a745;
|
|
color: white;
|
|
padding: 2px 4px;
|
|
border-radius: 2px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.template-card {
|
|
transition: all 0.3s;
|
|
cursor: pointer;
|
|
border-left: 4px solid var(--accent);
|
|
}
|
|
|
|
.template-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.step-indicator {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.step {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 10px;
|
|
border-bottom: 3px solid #dee2e6;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.step.active {
|
|
border-bottom-color: var(--accent);
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.step.completed {
|
|
border-bottom-color: #28a745;
|
|
color: #28a745;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1><i class="bi bi-puzzle me-2"></i>Template Builder</h1>
|
|
<p class="text-muted mb-0">Byg templates til automatisk faktura-udtrækning</p>
|
|
</div>
|
|
<a href="/billing/supplier-invoices" class="btn btn-outline-secondary">
|
|
<i class="bi bi-arrow-left me-2"></i>Tilbage til Kassekladde
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Step Indicator -->
|
|
<div class="step-indicator">
|
|
<div class="step active" id="step1">
|
|
<i class="bi bi-1-circle-fill me-2"></i>Vælg Fil
|
|
</div>
|
|
<div class="step" id="step2">
|
|
<i class="bi bi-2-circle me-2"></i>Vælg Leverandør
|
|
</div>
|
|
<div class="step" id="step3">
|
|
<i class="bi bi-3-circle me-2"></i>Definer Patterns
|
|
</div>
|
|
<div class="step" id="step4">
|
|
<i class="bi bi-4-circle me-2"></i>Test & Gem
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 1: Select File -->
|
|
<div class="card mb-4" id="stepContent1">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-file-earmark-pdf me-2"></i>Vælg Fil til Template</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row" id="filesList">
|
|
<!-- Files loaded dynamically -->
|
|
</div>
|
|
<div class="mt-3 text-end">
|
|
<button class="btn btn-outline-secondary" onclick="skipFileSelection()">
|
|
Spring over <i class="bi bi-arrow-right ms-2"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Select Vendor -->
|
|
<div class="card mb-4 d-none" id="stepContent2">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-building me-2"></i>Vælg Leverandør</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-5">
|
|
<h6>PDF Preview</h6>
|
|
<div class="pdf-preview" id="pdfPreview2" style="max-height: 400px;">
|
|
<!-- PDF text shown here -->
|
|
</div>
|
|
</div>
|
|
<div class="col-md-7">
|
|
<div class="mb-3">
|
|
<label class="form-label">Leverandør <span class="text-danger">*</span></label>
|
|
<select class="form-select" id="vendorSelect" required>
|
|
<option value="">-- Vælg leverandør --</option>
|
|
</select>
|
|
<small class="text-muted">Vælg den leverandør som fakturaen kommer fra</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Template Navn <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="templateName" placeholder="F.eks. 'BMC Standard Faktura'" required>
|
|
<small class="text-muted">Navn på templaten, f.eks. leverandør + "Standard" eller "Email faktura"</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Produktkategori <span class="text-danger">*</span></label>
|
|
<select class="form-select" id="productCategory" required>
|
|
<option value="varesalg">🛒 Varesalg (videresalg af hardware)</option>
|
|
<option value="drift">🔧 Drift (internet, hosting, cloud services)</option>
|
|
<option value="anlæg">🏗️ Anlæg (investeringer, infrastruktur)</option>
|
|
<option value="abonnement">📅 Abonnement (løbende services)</option>
|
|
<option value="lager">📦 Lager (lagervarer)</option>
|
|
<option value="udlejning">🏪 Udlejning</option>
|
|
</select>
|
|
<small class="text-muted">Standardkategori for varelinjer fra denne leverandør</small>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="validateAndNextStep(3)">
|
|
Næste <i class="bi bi-arrow-right ms-2"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Define Patterns -->
|
|
<div class="card mb-4 d-none" id="stepContent3">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-code-slash me-2"></i>Definer Udtrækningsmønstre</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-5">
|
|
<h6>PDF Tekst Preview</h6>
|
|
|
|
<!-- File selector for editing mode -->
|
|
<div class="mb-3" id="editFileSelector" style="display:none;">
|
|
<label class="form-label">Vælg faktura at vise:</label>
|
|
<select class="form-select form-select-sm" id="editFileSelect" onchange="loadSelectedFile()">
|
|
<option value="">-- Vælg fil --</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
<strong>Sådan gør du:</strong><br>
|
|
1. Klik "🤖 AI Auto-generer" for at lade AI finde alle felter automatisk<br>
|
|
2. Eller markér tekst manuelt og vælg felttype<br>
|
|
3. Systemet laver automatisk patterns!
|
|
</div>
|
|
|
|
<button class="btn btn-success w-100 mb-3" onclick="autoGenerateTemplate()" id="aiGenerateBtn">
|
|
<i class="bi bi-magic me-2"></i>🤖 AI Auto-generer Template
|
|
</button>
|
|
|
|
<!-- Selection buttons - always visible -->
|
|
<div class="mb-3 p-3 border rounded bg-light">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<strong>Markeret tekst:</strong>
|
|
<span id="selectedTextDisplay" class="badge bg-secondary">Ingen</span>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong class="small">Hoved-felter:</strong>
|
|
</div>
|
|
<div class="btn-group w-100 mb-2" role="group">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="setField('invoice_number')" title="Sæt som fakturanummer">
|
|
<i class="bi bi-hash"></i> Nr
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="setField('invoice_date')" title="Sæt som dato">
|
|
<i class="bi bi-calendar"></i> Dato
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="setField('total_amount')" title="Sæt som beløb">
|
|
<i class="bi bi-currency-dollar"></i> Beløb
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="setField('cvr')" title="Sæt som CVR">
|
|
<i class="bi bi-building"></i> CVR
|
|
</button>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong class="small">Varelinjer:</strong>
|
|
</div>
|
|
<div class="btn-group w-100" role="group">
|
|
<button class="btn btn-sm btn-outline-info" onclick="setLineField('start')" title="Start markør for linjer">
|
|
<i class="bi bi-arrow-down-circle"></i> Start
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-info" onclick="setLineField('end')" title="Slut markør for linjer">
|
|
<i class="bi bi-arrow-up-circle"></i> Slut
|
|
</button>
|
|
<button class="btn btn-sm btn-success" onclick="addDetectionFromSelection()" title="Tilføj som detektionsmønster">
|
|
<i class="bi bi-plus-circle"></i> Detektion
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pdf-preview" id="pdfPreview" onmouseup="handleTextSelection()">
|
|
<!-- PDF text shown here -->
|
|
</div>
|
|
</div>
|
|
<div class="col-md-7">
|
|
<h6>Udtrækningsmønstre</h6>
|
|
|
|
<!-- Detection Patterns -->
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">Detektionsmønstre</label>
|
|
<small class="d-block text-muted mb-2">Tekststrenge der identificerer leverandøren/layout</small>
|
|
<div id="detectionList" class="mb-2" style="min-height: 40px;">
|
|
<span class="text-muted fst-italic">Markér tekst i PDF og klik "Detektion" knappen</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Field Patterns -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Fakturanummer</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="invoiceNumberValue" readonly placeholder="Markér i PDF og klik 'Fakturanummer'">
|
|
<button class="btn btn-outline-danger" onclick="clearField('invoice_number')">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
<input type="hidden" id="invoiceNumberPattern">
|
|
<div id="test_invoice_number" class="pattern-test d-none"></div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Dato</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="dateValue" readonly placeholder="Markér dato i PDF">
|
|
<button class="btn btn-outline-danger" onclick="clearField('invoice_date')">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
<input type="hidden" id="datePattern">
|
|
<div id="test_invoice_date" class="pattern-test d-none"></div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Total Beløb</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="totalValue" readonly placeholder="Markér beløb i PDF">
|
|
<button class="btn btn-outline-danger" onclick="clearField('total_amount')">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
<input type="hidden" id="totalPattern">
|
|
<div id="test_total_amount" class="pattern-test d-none"></div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">CVR Nummer (valgfri)</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="cvrValue" readonly placeholder="Markér CVR i PDF">
|
|
<button class="btn btn-outline-danger" onclick="clearField('cvr')">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
<input type="hidden" id="cvrPattern">
|
|
<div id="test_cvr" class="pattern-test d-none"></div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h6 class="mb-3">Varelinjer Udtrækning (valgfri)</h6>
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
<small><strong>Avanceret:</strong> Definer hvordan varelinjer skal findes i PDF'en. Dette er valgfrit - du kan også tilføje linjer manuelt senere.</small>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Start markør for linjer</label>
|
|
<input type="text" class="form-control form-control-sm" id="linesStartPattern"
|
|
placeholder='F.eks. "Nr.VarenrTekst" eller "Pos\s+Varenr"'>
|
|
<small class="text-muted">Tekst der vises lige før første varelinje starter</small>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Slut markør for linjer</label>
|
|
<input type="text" class="form-control form-control-sm" id="linesEndPattern"
|
|
placeholder='F.eks. "Subtotal" eller "I alt ekskl"'>
|
|
<small class="text-muted">Tekst der vises efter sidste varelinje</small>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Linje pattern (regex)</label>
|
|
<input type="text" class="form-control form-control-sm" id="lineItemPattern"
|
|
placeholder='Klik "Hjælp" for eksempler'>
|
|
<small class="text-muted">Regex til at udtrække: varenummer, beskrivelse, antal, pris fra hver linje</small>
|
|
<button class="btn btn-sm btn-info mt-2" type="button" data-bs-toggle="collapse" data-bs-target="#patternHelp">
|
|
<i class="bi bi-question-circle me-1"></i>Hjælp med patterns
|
|
</button>
|
|
<div class="collapse mt-2" id="patternHelp">
|
|
<div class="card card-body bg-light small">
|
|
<h6>Eksempel fra din PDF:</h6>
|
|
<pre class="mb-2">195006Betalingsmetode (Kortbetaling) 141,2041,20</pre>
|
|
|
|
<h6>Pattern forklaring:</h6>
|
|
<table class="table table-sm table-bordered">
|
|
<tr>
|
|
<th>Del</th>
|
|
<th>Pattern</th>
|
|
<th>Beskrivelse</th>
|
|
</tr>
|
|
<tr>
|
|
<td>Linjenr</td>
|
|
<td><code>^\d+</code></td>
|
|
<td>Start af linje, tal</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Varenr</td>
|
|
<td><code>(\S+)</code></td>
|
|
<td>Gruppe 1: Varenummer (ingen mellemrum)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Beskrivelse</td>
|
|
<td><code>(.+?)</code></td>
|
|
<td>Gruppe 2: Tekst (lazy match)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Antal</td>
|
|
<td><code>([\d.,]+)</code></td>
|
|
<td>Gruppe 3: Tal med komma/punktum</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Pris</td>
|
|
<td><code>([\d.,]+)</code></td>
|
|
<td>Gruppe 4: Tal med komma/punktum</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Beløb</td>
|
|
<td><code>([\d.,]+)</code></td>
|
|
<td>Gruppe 5: Tal med komma/punktum</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h6>Komplet pattern eksempel:</h6>
|
|
<div class="bg-white p-2 border rounded mb-2">
|
|
<code>^\d+(\S+)\s+(.+?)\s+([\d.,]+)([\d.,]+)([\d.,]+)$</code>
|
|
</div>
|
|
|
|
<h6>Simplere variant (kun varenr og beskrivelse):</h6>
|
|
<div class="bg-white p-2 border rounded mb-2">
|
|
<code>^\d+(\S+)\s+(.+)$</code>
|
|
</div>
|
|
|
|
<button class="btn btn-sm btn-success mt-2" onclick="document.getElementById('lineItemPattern').value = '^\\\\d+(\\\\S+)\\\\s+(.+?)\\\\s+([\\\\d.,]+)([\\\\d.,]+)([\\\\d.,]+)$'">
|
|
<i class="bi bi-clipboard me-1"></i>Brug komplet pattern
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success mt-2 ms-2" onclick="document.getElementById('lineItemPattern').value = '^\\\\d+(\\\\S+)\\\\s+(.+)$'">
|
|
<i class="bi bi-clipboard me-1"></i>Brug simpel pattern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn btn-primary" onclick="validateAndNextStep(4)">
|
|
Næste <i class="bi bi-arrow-right ms-2"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 4: Test & Save -->
|
|
<div class="card mb-4 d-none" id="stepContent4">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-check-circle me-2"></i>Test & Gem Template</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-5">
|
|
<h6>PDF Preview</h6>
|
|
<div class="pdf-preview" id="pdfPreview4" style="max-height: 500px;">
|
|
<!-- PDF text shown here -->
|
|
</div>
|
|
</div>
|
|
<div class="col-md-7">
|
|
<div id="templateSummary" class="mb-4">
|
|
<!-- Summary shown here -->
|
|
</div>
|
|
|
|
<div id="testResults" class="alert d-none mb-3" role="alert">
|
|
<!-- Test results shown here -->
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-info" onclick="testTemplate()">
|
|
<i class="bi bi-flask me-2"></i>Test Template
|
|
</button>
|
|
<button class="btn btn-success btn-lg" onclick="saveTemplate()">
|
|
<i class="bi bi-save me-2"></i>Gem Template
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="nextStep(3)">
|
|
<i class="bi bi-arrow-left me-2"></i>Tilbage
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
let currentFile = null;
|
|
let pdfText = '';
|
|
let selectedText = '';
|
|
let detectionPatterns = [];
|
|
let fieldPatterns = {};
|
|
let editingTemplateId = null; // Track if we're editing
|
|
|
|
// Load pending files on page load
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// Check if we're editing an existing template
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
editingTemplateId = urlParams.get('id');
|
|
|
|
if (editingTemplateId) {
|
|
await loadExistingTemplate(editingTemplateId);
|
|
} else {
|
|
await loadPendingFiles();
|
|
await loadVendors();
|
|
|
|
// Check if we're creating a template for a specific vendor/file
|
|
const vendorIdParam = urlParams.get('vendor');
|
|
const fileIdParam = urlParams.get('file');
|
|
|
|
// Check for sessionStorage data (from supplier invoices page)
|
|
const storedData = sessionStorage.getItem('templateCreateData');
|
|
let targetFileId = fileIdParam;
|
|
let targetVendorId = vendorIdParam;
|
|
let targetFileName = null;
|
|
let targetPdfText = null;
|
|
|
|
if (storedData) {
|
|
try {
|
|
const data = JSON.parse(storedData);
|
|
console.log('🔄 Loaded template creation data from sessionStorage:', data);
|
|
|
|
// Override with sessionStorage if available
|
|
if (data.fileId) targetFileId = data.fileId;
|
|
if (data.vendorId) targetVendorId = data.vendorId;
|
|
if (data.pdfText) targetPdfText = data.pdfText;
|
|
targetFileName = data.fileName || data.vendorName || targetFileName;
|
|
|
|
// Clear sessionStorage after use
|
|
sessionStorage.removeItem('templateCreateData');
|
|
} catch (error) {
|
|
console.error('Failed to parse template creation data:', error);
|
|
}
|
|
}
|
|
|
|
// If we have PDF text from sessionStorage, skip file selection
|
|
if (targetPdfText && targetVendorId && targetFileId) {
|
|
console.log('🚀 Fast-track: Using PDF text from sessionStorage');
|
|
|
|
// Set up the file data directly
|
|
currentFile = {
|
|
file_id: targetFileId,
|
|
filename: targetFileName || `File ${targetFileId}`,
|
|
text: targetPdfText
|
|
};
|
|
pdfText = targetPdfText;
|
|
|
|
// Wait for vendors to load
|
|
setTimeout(() => {
|
|
// Pre-select vendor
|
|
const vendorSelect = document.getElementById('vendorSelect');
|
|
if (vendorSelect) {
|
|
vendorSelect.value = targetVendorId;
|
|
console.log('✅ Vendor pre-selected:', targetVendorId);
|
|
}
|
|
|
|
// Auto-generate template name
|
|
const templateNameInput = document.getElementById('templateName');
|
|
if (templateNameInput && !templateNameInput.value) {
|
|
const vendorName = vendorSelect?.options[vendorSelect.selectedIndex]?.text || 'Template';
|
|
templateNameInput.value = `${vendorName} Standard Template`;
|
|
console.log('✅ Template name generated:', templateNameInput.value);
|
|
}
|
|
|
|
// Show PDF preview in step 2
|
|
document.getElementById('pdfPreview2').textContent = pdfText;
|
|
|
|
// Go directly to step 2
|
|
console.log('🎯 Jumping to step 2 (vendor & template name)');
|
|
nextStep(2);
|
|
|
|
// After a moment, auto-advance to step 3
|
|
setTimeout(() => {
|
|
console.log('🚀 Auto-advancing to step 3 (pattern definition)');
|
|
validateAndNextStep(3);
|
|
}, 500);
|
|
|
|
}, 500);
|
|
}
|
|
// If we have a target file but no PDF text, try to select from pending list
|
|
else if (targetFileId) {
|
|
console.log(`🎯 Auto-selecting file ${targetFileId} (${targetFileName || 'unknown'})`);
|
|
|
|
// Wait for files to load, then auto-select
|
|
setTimeout(async () => {
|
|
try {
|
|
// First check if file exists in the loaded files
|
|
const filesList = document.getElementById('filesList');
|
|
console.log('📋 Files list HTML:', filesList.innerHTML.substring(0, 200));
|
|
|
|
// Try to select the file
|
|
console.log('🔄 Calling selectFile...');
|
|
await selectFile(parseInt(targetFileId), targetFileName || `File ${targetFileId}`);
|
|
console.log('✅ selectFile completed');
|
|
|
|
// After file is selected, pre-select vendor if available
|
|
if (targetVendorId) {
|
|
console.log(`🎯 Pre-selecting vendor ${targetVendorId}`);
|
|
|
|
// Wait a bit for step 2 to render
|
|
setTimeout(() => {
|
|
const vendorSelect = document.getElementById('vendorSelect');
|
|
if (!vendorSelect) {
|
|
console.error('❌ vendorSelect not found!');
|
|
return;
|
|
}
|
|
|
|
vendorSelect.value = targetVendorId;
|
|
console.log('✅ Vendor selected:', vendorSelect.value);
|
|
|
|
// If both file and vendor are set, auto-advance to step 3
|
|
setTimeout(() => {
|
|
const templateNameInput = document.getElementById('templateName');
|
|
if (!templateNameInput) {
|
|
console.error('❌ templateName input not found!');
|
|
return;
|
|
}
|
|
|
|
if (!templateNameInput.value) {
|
|
// Auto-generate template name if empty
|
|
const vendorName = vendorSelect.options[vendorSelect.selectedIndex]?.text || 'Template';
|
|
templateNameInput.value = `${vendorName} Standard Template`;
|
|
console.log('✅ Template name set:', templateNameInput.value);
|
|
}
|
|
|
|
console.log('🚀 Auto-advancing to step 3 (pattern definition)');
|
|
validateAndNextStep(3);
|
|
}, 300);
|
|
}, 300);
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Failed to auto-select file:', error);
|
|
alert('Kunne ikke auto-vælge fil: ' + error.message);
|
|
}
|
|
}, 1000); // Increased timeout to 1 second
|
|
}
|
|
}
|
|
});
|
|
|
|
async function loadExistingTemplate(templateId) {
|
|
try {
|
|
console.log('Loading template:', templateId);
|
|
|
|
// Load template data
|
|
const response = await fetch(`/api/v1/supplier-invoices/templates/${templateId}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const template = await response.json();
|
|
console.log('Template loaded:', template);
|
|
|
|
if (!template) {
|
|
console.error('Template not found:', templateId);
|
|
alert('Template ikke fundet');
|
|
window.location.href = '/billing/templates';
|
|
return;
|
|
}
|
|
|
|
console.log('Template found:', template.template_name);
|
|
|
|
// Update page title
|
|
document.querySelector('h1').innerHTML = `<i class="bi bi-pencil me-2"></i>Rediger Template: ${template.template_name}`;
|
|
|
|
// Populate template data
|
|
document.getElementById('templateName').value = template.template_name;
|
|
|
|
// Load vendors and select the correct one
|
|
await loadVendors();
|
|
document.getElementById('vendorSelect').value = template.vendor_id;
|
|
|
|
// Set product category
|
|
if (template.default_product_category) {
|
|
document.getElementById('productCategory').value = template.default_product_category;
|
|
}
|
|
|
|
// Load detection patterns
|
|
detectionPatterns = template.detection_patterns || [];
|
|
|
|
// Load field patterns
|
|
fieldPatterns = template.field_mappings || {};
|
|
|
|
// Skip to step 3 (patterns) - show the step first
|
|
document.getElementById('stepContent1').classList.add('d-none');
|
|
document.getElementById('stepContent2').classList.add('d-none');
|
|
document.getElementById('stepContent3').classList.remove('d-none');
|
|
|
|
document.getElementById('step1').classList.remove('active');
|
|
document.getElementById('step2').classList.remove('active');
|
|
document.getElementById('step3').classList.add('active');
|
|
|
|
// Wait a bit for DOM to be ready, then populate fields
|
|
setTimeout(() => {
|
|
renderFieldPatterns();
|
|
}, 100);
|
|
|
|
// Load files from this vendor to show PDF preview
|
|
const filesResponse = await fetch('/api/v1/pending-supplier-invoice-files');
|
|
const filesData = await filesResponse.json();
|
|
|
|
// Filter files by vendor
|
|
const vendorFiles = filesData.files.filter(f =>
|
|
f.vendor_matched_id == template.vendor_id ||
|
|
f.vendor_name == template.vendor_name
|
|
);
|
|
|
|
console.log(`Found ${vendorFiles.length} files for vendor ${template.vendor_name}`);
|
|
|
|
// Show file selector in edit mode and populate it
|
|
const fileSelector = document.getElementById('editFileSelector');
|
|
const fileSelect = document.getElementById('editFileSelect');
|
|
if (fileSelector && fileSelect) {
|
|
fileSelector.style.display = 'block';
|
|
fileSelect.innerHTML = '<option value="">-- Vælg fil --</option>';
|
|
|
|
vendorFiles.forEach(file => {
|
|
const option = document.createElement('option');
|
|
option.value = file.file_id;
|
|
option.textContent = `${file.filename} (${file.status})`;
|
|
fileSelect.appendChild(option);
|
|
});
|
|
|
|
// If no vendor files, show all files
|
|
if (vendorFiles.length === 0) {
|
|
filesData.files.forEach(file => {
|
|
const option = document.createElement('option');
|
|
option.value = file.file_id;
|
|
option.textContent = `${file.filename} - ${file.vendor_name || 'Ukendt'} (${file.status})`;
|
|
fileSelect.appendChild(option);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (vendorFiles.length > 0) {
|
|
const firstFile = vendorFiles[0];
|
|
if (fileSelect) {
|
|
fileSelect.value = firstFile.file_id;
|
|
}
|
|
const fileResponse = await fetch(`/api/v1/supplier-invoices/reprocess/${firstFile.file_id}`, {
|
|
method: 'POST'
|
|
});
|
|
const fileData = await fileResponse.json();
|
|
selectedText = fileData.pdf_text || '';
|
|
pdfText = selectedText; // Set pdfText for pattern testing
|
|
const pdfPreview = document.getElementById('pdfPreview');
|
|
if (pdfPreview) {
|
|
pdfPreview.textContent = selectedText;
|
|
}
|
|
} else {
|
|
console.log('No files found for this vendor - loading any file');
|
|
// Fallback to any file if no vendor-specific files
|
|
if (filesData.files.length > 0) {
|
|
const firstFile = filesData.files[0];
|
|
if (fileSelect) {
|
|
fileSelect.value = firstFile.file_id;
|
|
}
|
|
const fileResponse = await fetch(`/api/v1/supplier-invoices/reprocess/${firstFile.file_id}`, {
|
|
method: 'POST'
|
|
});
|
|
const fileData = await fileResponse.json();
|
|
selectedText = fileData.pdf_text || '';
|
|
pdfText = selectedText; // Set pdfText for pattern testing
|
|
const pdfPreview = document.getElementById('pdfPreview');
|
|
if (pdfPreview) {
|
|
pdfPreview.textContent = selectedText;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Template loaded successfully');
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load template:', error);
|
|
console.error('Error details:', error.message, error.stack);
|
|
alert(`Kunne ikke hente template: ${error.message}`);
|
|
window.location.href = '/billing/templates';
|
|
}
|
|
}
|
|
|
|
// Load selected file when user changes dropdown
|
|
async function loadSelectedFile() {
|
|
const fileSelect = document.getElementById('editFileSelect');
|
|
if (!fileSelect || !fileSelect.value) {
|
|
console.log('No file selected');
|
|
return;
|
|
}
|
|
|
|
const fileId = fileSelect.value;
|
|
console.log(`Loading file ${fileId}...`);
|
|
|
|
try {
|
|
const fileResponse = await fetch(`/api/v1/supplier-invoices/reprocess/${fileId}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (!fileResponse.ok) {
|
|
throw new Error(`HTTP ${fileResponse.status}: ${await fileResponse.text()}`);
|
|
}
|
|
|
|
const fileData = await fileResponse.json();
|
|
selectedText = fileData.pdf_text || '';
|
|
pdfText = selectedText; // Set pdfText for pattern testing
|
|
|
|
const pdfPreview = document.getElementById('pdfPreview');
|
|
if (pdfPreview) {
|
|
pdfPreview.textContent = selectedText;
|
|
console.log(`Loaded ${selectedText.length} characters from file`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load file:', error);
|
|
alert(`Kunne ikke hente fil: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function renderDetectionPatterns() {
|
|
// Detection patterns are stored in array, show them in UI somehow
|
|
// For now, just log them - you might want to add a display area
|
|
console.log('Detection patterns:', detectionPatterns);
|
|
}
|
|
|
|
function renderFieldPatterns() {
|
|
// Populate field pattern inputs - check if elements exist first
|
|
const invoiceNumberPattern = document.getElementById('invoiceNumberPattern');
|
|
const datePattern = document.getElementById('datePattern');
|
|
const totalPattern = document.getElementById('totalPattern');
|
|
const cvrPattern = document.getElementById('cvrPattern');
|
|
const linesStartPattern = document.getElementById('linesStartPattern');
|
|
const linesEndPattern = document.getElementById('linesEndPattern');
|
|
const lineItemPattern = document.getElementById('lineItemPattern');
|
|
|
|
if (fieldPatterns.invoice_number && invoiceNumberPattern) {
|
|
invoiceNumberPattern.value = fieldPatterns.invoice_number.pattern || '';
|
|
}
|
|
if (fieldPatterns.invoice_date && datePattern) {
|
|
datePattern.value = fieldPatterns.invoice_date.pattern || '';
|
|
}
|
|
if (fieldPatterns.total_amount && totalPattern) {
|
|
totalPattern.value = fieldPatterns.total_amount.pattern || '';
|
|
}
|
|
if (fieldPatterns.vendor_cvr && cvrPattern) {
|
|
cvrPattern.value = fieldPatterns.vendor_cvr.pattern || '';
|
|
}
|
|
if (fieldPatterns.lines_start && linesStartPattern) {
|
|
linesStartPattern.value = fieldPatterns.lines_start.pattern || '';
|
|
}
|
|
if (fieldPatterns.lines_end && linesEndPattern) {
|
|
linesEndPattern.value = fieldPatterns.lines_end.pattern || '';
|
|
}
|
|
if (fieldPatterns.line_item && lineItemPattern) {
|
|
lineItemPattern.value = fieldPatterns.line_item.pattern || '';
|
|
}
|
|
|
|
console.log('Field patterns populated');
|
|
}
|
|
|
|
async function loadPendingFiles() {
|
|
try {
|
|
const response = await fetch('/api/v1/pending-supplier-invoice-files');
|
|
const data = await response.json();
|
|
|
|
const filesList = document.getElementById('filesList');
|
|
filesList.innerHTML = '';
|
|
|
|
if (data.files.length === 0) {
|
|
filesList.innerHTML = '<p class="text-muted">Ingen ventende filer fundet</p>';
|
|
return;
|
|
}
|
|
|
|
data.files.forEach(file => {
|
|
filesList.innerHTML += `
|
|
<div class="col-md-4 mb-3">
|
|
<div class="card template-card" onclick="selectFile(${file.file_id}, '${file.filename}')">
|
|
<div class="card-body">
|
|
<h6><i class="bi bi-file-pdf text-danger me-2"></i>${file.filename}</h6>
|
|
<small class="text-muted">File ID: ${file.file_id}</small><br>
|
|
<span class="badge bg-${file.status === 'failed' ? 'danger' : 'warning'} mt-2">
|
|
${file.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load files:', error);
|
|
alert('Kunne ikke hente filer');
|
|
}
|
|
}
|
|
|
|
async function loadVendors() {
|
|
try {
|
|
const response = await fetch('/api/v1/vendors');
|
|
const vendors = await response.json();
|
|
|
|
const select = document.getElementById('vendorSelect');
|
|
vendors.forEach(vendor => {
|
|
select.innerHTML += `<option value="${vendor.id}">${vendor.name}</option>`;
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load vendors:', error);
|
|
}
|
|
}
|
|
|
|
async function selectFile(fileId, filename) {
|
|
try {
|
|
console.log(`🔄 Selecting file: ${fileId} (${filename})`);
|
|
|
|
// Get PDF text directly (fast endpoint, no AI processing)
|
|
console.log(`📡 Fetching: /api/v1/supplier-invoices/files/${fileId}/pdf-text`);
|
|
const response = await fetch(`/api/v1/supplier-invoices/files/${fileId}/pdf-text`);
|
|
|
|
console.log(`📥 Response status: ${response.status}`);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error(`❌ HTTP error: ${response.status} - ${errorText}`);
|
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('📦 Response data:', data);
|
|
|
|
if (!data.pdf_text) {
|
|
console.warn('⚠️ No PDF text in response');
|
|
}
|
|
|
|
currentFile = {
|
|
file_id: fileId,
|
|
filename: filename,
|
|
text: data.pdf_text || ''
|
|
};
|
|
|
|
pdfText = data.pdf_text || '';
|
|
|
|
console.log(`✅ File loaded, PDF text length: ${pdfText.length} chars`);
|
|
|
|
// Show PDF preview
|
|
const pdfPreview = document.getElementById('pdfPreview');
|
|
if (pdfPreview) {
|
|
pdfPreview.textContent = pdfText;
|
|
}
|
|
|
|
console.log('🚀 Advancing to step 2');
|
|
nextStep(2);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to load file:', error);
|
|
alert('Kunne ikke hente fil: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function skipFileSelection() {
|
|
// Allow user to proceed without selecting a file
|
|
// They can upload/paste PDF text later
|
|
console.log('⏭️ Skipping file selection');
|
|
|
|
currentFile = null;
|
|
pdfText = '';
|
|
|
|
nextStep(2);
|
|
}
|
|
|
|
function validateAndNextStep(targetStep) {
|
|
// Validate step 2 fields
|
|
if (targetStep === 3) {
|
|
const vendorId = document.getElementById('vendorSelect').value;
|
|
const templateName = document.getElementById('templateName').value.trim();
|
|
|
|
if (!vendorId) {
|
|
alert('Vælg en leverandør før du går videre');
|
|
document.getElementById('vendorSelect').focus();
|
|
return;
|
|
}
|
|
|
|
if (!templateName) {
|
|
alert('Angiv et template navn før du går videre');
|
|
document.getElementById('templateName').focus();
|
|
return;
|
|
}
|
|
}
|
|
|
|
nextStep(targetStep);
|
|
}
|
|
|
|
function nextStep(step) {
|
|
// Hide all steps
|
|
for (let i = 1; i <= 4; i++) {
|
|
document.getElementById(`stepContent${i}`).classList.add('d-none');
|
|
document.getElementById(`step${i}`).classList.remove('active');
|
|
}
|
|
|
|
// Show target step
|
|
document.getElementById(`stepContent${step}`).classList.remove('d-none');
|
|
document.getElementById(`step${step}`).classList.add('active');
|
|
|
|
// Mark previous steps as completed
|
|
for (let i = 1; i < step; i++) {
|
|
document.getElementById(`step${i}`).classList.add('completed');
|
|
}
|
|
|
|
// Sync PDF preview to all steps
|
|
if (pdfText) {
|
|
if (step === 2) {
|
|
document.getElementById('pdfPreview2').textContent = pdfText;
|
|
} else if (step === 4) {
|
|
document.getElementById('pdfPreview4').textContent = pdfText;
|
|
}
|
|
}
|
|
|
|
// If step 4, show summary
|
|
if (step === 4) {
|
|
showSummary();
|
|
}
|
|
}
|
|
|
|
function handleTextSelection() {
|
|
const selection = window.getSelection();
|
|
const text = selection.toString().trim();
|
|
|
|
if (text.length > 0) {
|
|
selectedText = text;
|
|
document.getElementById('selectedTextDisplay').textContent = text.substring(0, 30) + (text.length > 30 ? '...' : '');
|
|
document.getElementById('selectedTextDisplay').classList.remove('bg-secondary');
|
|
document.getElementById('selectedTextDisplay').classList.add('bg-success');
|
|
} else {
|
|
selectedText = '';
|
|
document.getElementById('selectedTextDisplay').textContent = 'Ingen';
|
|
document.getElementById('selectedTextDisplay').classList.remove('bg-success');
|
|
document.getElementById('selectedTextDisplay').classList.add('bg-secondary');
|
|
}
|
|
}
|
|
|
|
function setField(fieldName) {
|
|
if (!selectedText) {
|
|
alert('Markér først noget tekst i PDF\'en!');
|
|
return;
|
|
}
|
|
|
|
console.log('setField called:', { fieldName, selectedText });
|
|
|
|
// Auto-generate regex pattern based on selected text
|
|
const pattern = generatePattern(selectedText, fieldName);
|
|
|
|
console.log('Generated pattern:', pattern);
|
|
|
|
// Store pattern
|
|
fieldPatterns[fieldName] = {
|
|
value: selectedText,
|
|
pattern: pattern
|
|
};
|
|
|
|
console.log('Stored in fieldPatterns:', fieldPatterns[fieldName]);
|
|
|
|
// Update UI
|
|
if (fieldName === 'invoice_number') {
|
|
document.getElementById('invoiceNumberValue').value = selectedText;
|
|
document.getElementById('invoiceNumberPattern').value = pattern;
|
|
testGeneratedPattern('invoice_number', pattern);
|
|
} else if (fieldName === 'invoice_date') {
|
|
document.getElementById('dateValue').value = selectedText;
|
|
document.getElementById('datePattern').value = pattern;
|
|
testGeneratedPattern('invoice_date', pattern);
|
|
} else if (fieldName === 'total_amount') {
|
|
document.getElementById('totalValue').value = selectedText;
|
|
document.getElementById('totalPattern').value = pattern;
|
|
testGeneratedPattern('total_amount', pattern);
|
|
} else if (fieldName === 'cvr') {
|
|
document.getElementById('cvrValue').value = selectedText;
|
|
document.getElementById('cvrPattern').value = pattern;
|
|
testGeneratedPattern('cvr', pattern);
|
|
}
|
|
|
|
// Show success feedback
|
|
const fieldNames = {
|
|
'invoice_number': 'Fakturanummer',
|
|
'invoice_date': 'Dato',
|
|
'total_amount': 'Beløb',
|
|
'cvr': 'CVR'
|
|
};
|
|
|
|
// Brief success message
|
|
const badge = document.getElementById('selectedTextDisplay');
|
|
const originalText = badge.textContent;
|
|
badge.textContent = '✓ ' + fieldNames[fieldName] + ' sat!';
|
|
badge.classList.remove('bg-success');
|
|
badge.classList.add('bg-primary');
|
|
|
|
setTimeout(() => {
|
|
selectedText = '';
|
|
badge.textContent = 'Ingen';
|
|
badge.classList.remove('bg-primary');
|
|
badge.classList.add('bg-secondary');
|
|
window.getSelection().removeAllRanges();
|
|
}, 1500);
|
|
}
|
|
|
|
function setLineField(lineFieldType) {
|
|
if (!selectedText) {
|
|
alert('Markér først noget tekst i PDF\'en!');
|
|
return;
|
|
}
|
|
|
|
// Simple escape function for special regex chars
|
|
const escape = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
// Set the appropriate field
|
|
if (lineFieldType === 'start') {
|
|
document.getElementById('linesStartPattern').value = escape(selectedText);
|
|
} else if (lineFieldType === 'end') {
|
|
document.getElementById('linesEndPattern').value = escape(selectedText);
|
|
}
|
|
|
|
// Show success feedback
|
|
const badge = document.getElementById('selectedTextDisplay');
|
|
const label = lineFieldType === 'start' ? 'Start markør' : 'Slut markør';
|
|
badge.textContent = '✓ ' + label + ' sat!';
|
|
badge.classList.remove('bg-success');
|
|
badge.classList.add('bg-info');
|
|
|
|
setTimeout(() => {
|
|
selectedText = '';
|
|
badge.textContent = 'Ingen';
|
|
badge.classList.remove('bg-info');
|
|
badge.classList.add('bg-secondary');
|
|
window.getSelection().removeAllRanges();
|
|
}, 1500);
|
|
}
|
|
|
|
function generatePattern(text, fieldName) {
|
|
console.log('generatePattern called:', { text, fieldName });
|
|
console.log('pdfText length:', pdfText.length);
|
|
|
|
// Split selected text into words to find label and value
|
|
const words = text.trim().split(/\s+/);
|
|
console.log('Selected text words:', words);
|
|
|
|
let label = '';
|
|
let value = '';
|
|
|
|
// For invoice_number, date, amount: first word is usually the label
|
|
if (fieldName === 'invoice_number') {
|
|
// Try to find number in selected text
|
|
const numberMatch = text.match(/(\d+)/);
|
|
console.log('Number match:', numberMatch);
|
|
if (numberMatch) {
|
|
value = numberMatch[1];
|
|
// Find word before the number
|
|
const beforeNumber = text.substring(0, text.indexOf(value)).trim();
|
|
console.log('Before number:', beforeNumber);
|
|
const labelWords = beforeNumber.split(/\s+/);
|
|
console.log('Label words:', labelWords);
|
|
label = labelWords[labelWords.length - 1] || 'Nummer';
|
|
console.log('Using label:', label);
|
|
|
|
const pattern = `${escapeRegex(label)}\\s+(\\d+)`;
|
|
console.log('Invoice number pattern:', pattern);
|
|
return pattern;
|
|
} else {
|
|
console.log('No number found in selected text!');
|
|
}
|
|
} else if (fieldName === 'invoice_date') {
|
|
// Find date in selected text
|
|
const dateMatch = text.match(/(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/);
|
|
if (dateMatch) {
|
|
value = dateMatch[1];
|
|
const beforeDate = text.substring(0, text.indexOf(value)).trim();
|
|
const labelWords = beforeDate.split(/\s+/);
|
|
label = labelWords[labelWords.length - 1] || 'Dato';
|
|
|
|
const pattern = `${escapeRegex(label)}\\s+(\\d{1,2}[\\/.\\-]\\d{1,2}[\\/.\\-]\\d{2,4})`;
|
|
console.log('Date pattern:', pattern);
|
|
return pattern;
|
|
}
|
|
} else if (fieldName === 'total_amount') {
|
|
// Find amount in selected text
|
|
const amountMatch = text.match(/([\d.,]+)\s*$/);
|
|
if (amountMatch) {
|
|
value = amountMatch[1];
|
|
const beforeAmount = text.substring(0, text.indexOf(value)).trim();
|
|
const labelWords = beforeAmount.split(/\s+/);
|
|
label = labelWords[labelWords.length - 1] || 'beløb';
|
|
|
|
const pattern = `${escapeRegex(label)}\\s+([\\d.,]+)`;
|
|
console.log('Amount pattern:', pattern);
|
|
return pattern;
|
|
}
|
|
} else if (fieldName === 'cvr') {
|
|
// Find CVR number (8 digits, possibly with DK prefix)
|
|
const cvrMatch = text.match(/DK(\d{8})|(\d{8})/);
|
|
if (cvrMatch) {
|
|
const beforeCvr = text.substring(0, text.indexOf(cvrMatch[1] || cvrMatch[2])).trim();
|
|
const labelWords = beforeCvr.split(/\s+/);
|
|
label = labelWords[labelWords.length - 1] || 'CVR';
|
|
|
|
const pattern = `${escapeRegex(label)}\\s+\\w*(\\d{8})`;
|
|
console.log('CVR pattern:', pattern);
|
|
return pattern;
|
|
}
|
|
}
|
|
|
|
// Fallback: use first word as label
|
|
if (words.length >= 2) {
|
|
label = words[0];
|
|
const pattern = `${escapeRegex(label)}\\s+(.+?)`;
|
|
console.log('Fallback pattern:', pattern);
|
|
return pattern;
|
|
}
|
|
|
|
// Ultimate fallback
|
|
return escapeRegex(text);
|
|
}
|
|
|
|
function escapeRegex(str) {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function testGeneratedPattern(fieldName, pattern) {
|
|
const testDiv = document.getElementById(`test_${fieldName}`);
|
|
|
|
try {
|
|
const regex = new RegExp(pattern, 'i');
|
|
const match = pdfText.match(regex);
|
|
|
|
if (match) {
|
|
const value = match[1] || match[0];
|
|
testDiv.classList.remove('d-none');
|
|
testDiv.innerHTML = `<i class="bi bi-check-circle text-success me-2"></i>Pattern virker! Finder: <span class="match-highlight">${value}</span>`;
|
|
} else {
|
|
testDiv.classList.remove('d-none');
|
|
testDiv.innerHTML = '<i class="bi bi-x-circle text-danger me-2"></i>Pattern virker ikke - prøv at markere igen med mere kontekst';
|
|
}
|
|
} catch (e) {
|
|
testDiv.classList.remove('d-none');
|
|
testDiv.innerHTML = `<i class="bi bi-exclamation-triangle text-warning me-2"></i>Pattern fejl: ${e.message}`;
|
|
}
|
|
}
|
|
|
|
function clearField(fieldName) {
|
|
if (fieldName === 'invoice_number') {
|
|
document.getElementById('invoiceNumberValue').value = '';
|
|
document.getElementById('invoiceNumberPattern').value = '';
|
|
document.getElementById('test_invoice_number').classList.add('d-none');
|
|
} else if (fieldName === 'invoice_date') {
|
|
document.getElementById('dateValue').value = '';
|
|
document.getElementById('datePattern').value = '';
|
|
document.getElementById('test_invoice_date').classList.add('d-none');
|
|
} else if (fieldName === 'total_amount') {
|
|
document.getElementById('totalValue').value = '';
|
|
document.getElementById('totalPattern').value = '';
|
|
document.getElementById('test_total_amount').classList.add('d-none');
|
|
} else if (fieldName === 'cvr') {
|
|
document.getElementById('cvrValue').value = '';
|
|
document.getElementById('cvrPattern').value = '';
|
|
document.getElementById('test_cvr').classList.add('d-none');
|
|
}
|
|
delete fieldPatterns[fieldName];
|
|
}
|
|
|
|
function addDetectionFromSelection() {
|
|
if (!selectedText) {
|
|
alert('Markér først noget tekst i PDF\'en!');
|
|
return;
|
|
}
|
|
|
|
if (!detectionPatterns.includes(selectedText)) {
|
|
detectionPatterns.push(selectedText);
|
|
updateDetectionList();
|
|
|
|
// Show success feedback
|
|
const badge = document.getElementById('selectedTextDisplay');
|
|
badge.textContent = '✓ Tilføjet!';
|
|
badge.classList.remove('bg-success');
|
|
badge.classList.add('bg-primary');
|
|
|
|
setTimeout(() => {
|
|
selectedText = '';
|
|
badge.textContent = 'Ingen';
|
|
badge.classList.remove('bg-primary');
|
|
badge.classList.add('bg-secondary');
|
|
window.getSelection().removeAllRanges();
|
|
}, 1500);
|
|
} else {
|
|
alert('Denne tekst er allerede tilføjet som detektionsmønster');
|
|
}
|
|
}
|
|
|
|
function updateDetectionList() {
|
|
const list = document.getElementById('detectionList');
|
|
|
|
if (detectionPatterns.length === 0) {
|
|
list.innerHTML = '<span class="text-muted fst-italic">Markér tekst i PDF og klik "Detektion" knappen</span>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = '';
|
|
detectionPatterns.forEach((pattern, index) => {
|
|
list.innerHTML += `
|
|
<span class="badge bg-success me-2 mb-2" style="font-size: 0.9em;">
|
|
${pattern}
|
|
<i class="bi bi-x ms-1" style="cursor: pointer;" onclick="removeDetection(${index})" title="Fjern"></i>
|
|
</span>
|
|
`;
|
|
});
|
|
}
|
|
|
|
function removeDetection(index) {
|
|
detectionPatterns.splice(index, 1);
|
|
updateDetectionList();
|
|
}
|
|
|
|
function testPattern(inputId, fieldName) {
|
|
const pattern = document.getElementById(inputId).value;
|
|
const testDiv = document.getElementById(`test_${fieldName}`);
|
|
|
|
if (!pattern) {
|
|
testDiv.classList.add('d-none');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const regex = new RegExp(pattern, 'i');
|
|
const match = pdfText.match(regex);
|
|
|
|
if (match) {
|
|
const value = match[1] || match[0];
|
|
testDiv.classList.remove('d-none');
|
|
testDiv.innerHTML = `<i class="bi bi-check-circle text-success me-2"></i>Match fundet: <span class="match-highlight">${value}</span>`;
|
|
} else {
|
|
testDiv.classList.remove('d-none');
|
|
testDiv.innerHTML = '<i class="bi bi-x-circle text-danger me-2"></i>Ingen match fundet';
|
|
}
|
|
} catch (e) {
|
|
testDiv.classList.remove('d-none');
|
|
testDiv.innerHTML = `<i class="bi bi-exclamation-triangle text-warning me-2"></i>Ugyldig regex: ${e.message}`;
|
|
}
|
|
}
|
|
|
|
function showSummary() {
|
|
const vendorId = document.getElementById('vendorSelect').value;
|
|
const vendorName = document.getElementById('vendorSelect').selectedOptions[0].text;
|
|
const templateName = document.getElementById('templateName').value;
|
|
|
|
const invoiceNumberPattern = document.getElementById('invoiceNumberPattern').value;
|
|
const datePattern = document.getElementById('datePattern').value;
|
|
const totalPattern = document.getElementById('totalPattern').value;
|
|
const cvrPattern = document.getElementById('cvrPattern').value;
|
|
|
|
const linesStartPattern = document.getElementById('linesStartPattern').value;
|
|
const linesEndPattern = document.getElementById('linesEndPattern').value;
|
|
const lineItemPattern = document.getElementById('lineItemPattern').value;
|
|
|
|
const hasLineExtraction = linesStartPattern || linesEndPattern || lineItemPattern;
|
|
|
|
document.getElementById('templateSummary').innerHTML = `
|
|
<h5>Template Opsummering</h5>
|
|
<table class="table table-bordered">
|
|
<tr><th width="200">Leverandør</th><td>${vendorName}</td></tr>
|
|
<tr><th>Template Navn</th><td>${templateName}</td></tr>
|
|
<tr><th>Detektionsmønstre</th><td>${detectionPatterns.length} patterns defineret</td></tr>
|
|
<tr><th>Fakturanummer</th><td>${fieldPatterns.invoice_number?.value || '<em>ikke defineret</em>'}</td></tr>
|
|
<tr><th>Dato</th><td>${fieldPatterns.invoice_date?.value || '<em>ikke defineret</em>'}</td></tr>
|
|
<tr><th>Total</th><td>${fieldPatterns.total_amount?.value || '<em>ikke defineret</em>'}</td></tr>
|
|
<tr><th>CVR</th><td>${fieldPatterns.cvr?.value || '<em>ikke defineret</em>'}</td></tr>
|
|
<tr><th>Varelinjer</th><td>${hasLineExtraction ? '✅ Automatisk udtrækning' : '⚠️ Manuel indtastning'}</td></tr>
|
|
</table>
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-lightbulb me-2"></i>
|
|
<strong>Tips:</strong> Når du gemmer templaten, vil den automatisk blive brugt til at udtrække data fra fremtidige fakturaer fra denne leverandør.
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function autoGenerateTemplate() {
|
|
if (!pdfText) {
|
|
alert('Ingen PDF tekst tilgængelig');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('aiGenerateBtn');
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>AI analyserer PDF...';
|
|
|
|
try {
|
|
// Call Ollama to analyze the invoice
|
|
const response = await fetch('/api/v1/supplier-invoices/ai-analyze', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
pdf_text: pdfText,
|
|
vendor_id: document.getElementById('vendorSelect').value
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('AI analyse fejlede');
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// Helper to extract value (handles both nested {value, pattern} and flat formats)
|
|
const getValue = (field) => field?.value || field;
|
|
const getPattern = (field, defaultPattern) => field?.pattern || defaultPattern;
|
|
|
|
// Apply AI suggestions - handle both nested and flat responses
|
|
if (result.invoice_number) {
|
|
const value = getValue(result.invoice_number);
|
|
const pattern = getPattern(result.invoice_number, `(?:Nummer|Faktura|Invoice)\\s*(${value})`);
|
|
document.getElementById('invoiceNumberValue').value = value || '';
|
|
document.getElementById('invoiceNumberPattern').value = pattern;
|
|
fieldPatterns.invoice_number = { value, pattern };
|
|
testGeneratedPattern('invoice_number', pattern);
|
|
}
|
|
|
|
if (result.invoice_date) {
|
|
const value = getValue(result.invoice_date);
|
|
const pattern = getPattern(result.invoice_date, `Dato\\s*(\\d{1,2}[\\/.\\-]\\d{1,2}[\\/.\\-]\\d{2,4})`);
|
|
document.getElementById('dateValue').value = value || '';
|
|
document.getElementById('datePattern').value = pattern;
|
|
fieldPatterns.invoice_date = { value, pattern };
|
|
testGeneratedPattern('invoice_date', pattern);
|
|
}
|
|
|
|
if (result.total_amount) {
|
|
const value = getValue(result.total_amount);
|
|
// Try multiple patterns in order of specificity
|
|
let pattern = getPattern(result.total_amount, null);
|
|
if (!pattern) {
|
|
// Pattern 1: Multi-line "Totalbeløb DKK\n...1.471,20"
|
|
const multiLinePattern = `Totalbeløb\\s+DKK\\s*\\n.*?([\\d.,]+)\\s*$`;
|
|
// Pattern 2: Same line "Total: 1.471,20"
|
|
const sameLinePattern = `(?:Total|I alt|Totalbeløb|Beløb)\\s*(?:DKK)?\\s*:?\\s*([\\d.,]+)`;
|
|
// Test both and use the one that works
|
|
const testML = pdfText.match(new RegExp(multiLinePattern, 'm'));
|
|
const testSL = pdfText.match(new RegExp(sameLinePattern, 'i'));
|
|
pattern = testML ? multiLinePattern : sameLinePattern;
|
|
}
|
|
document.getElementById('totalValue').value = value || '';
|
|
document.getElementById('totalPattern').value = pattern;
|
|
fieldPatterns.total_amount = { value, pattern };
|
|
testGeneratedPattern('total_amount', pattern);
|
|
}
|
|
|
|
if (result.cvr || result.vendor_cvr) {
|
|
const cvrField = result.vendor_cvr || result.cvr;
|
|
const value = getValue(cvrField);
|
|
const pattern = getPattern(cvrField, `(?:CVR|Momsnr|DK)\\s*(\\d{8})`);
|
|
document.getElementById('cvrValue').value = value || '';
|
|
document.getElementById('cvrPattern').value = pattern;
|
|
fieldPatterns.cvr = { value, pattern };
|
|
testGeneratedPattern('cvr', pattern);
|
|
}
|
|
|
|
// Detection patterns - handle both string array and object array
|
|
if (result.detection_patterns && result.detection_patterns.length > 0) {
|
|
detectionPatterns = result.detection_patterns.map(p =>
|
|
typeof p === 'string' ? p : p.pattern
|
|
);
|
|
updateDetectionList();
|
|
} else {
|
|
// Fallback: Extract company name from CVR match or use generic patterns
|
|
detectionPatterns = ['Faktura'];
|
|
if (result.cvr || result.vendor_cvr) {
|
|
// Try to find company name near CVR
|
|
const cvrMatch = pdfText.match(/([A-ZÆØÅ][A-Za-zæøåÆØÅ\s&]+(?:ApS|A\/S|AS|IVS))/);
|
|
if (cvrMatch) {
|
|
detectionPatterns = [cvrMatch[1].trim(), 'Faktura'];
|
|
}
|
|
}
|
|
updateDetectionList();
|
|
}
|
|
|
|
// Line extraction - handle both nested and flat
|
|
if (result.lines_start) {
|
|
const linesStart = result.lines_start?.pattern || result.lines_start;
|
|
document.getElementById('linesStartPattern').value = linesStart;
|
|
}
|
|
if (result.lines_end) {
|
|
const linesEnd = result.lines_end?.pattern || result.lines_end;
|
|
document.getElementById('linesEndPattern').value = linesEnd;
|
|
}
|
|
// Note: Intentionally NOT setting line_pattern - multi-line extraction handles it
|
|
|
|
btn.innerHTML = '<i class="bi bi-check-circle me-2"></i>✅ AI analyse færdig!';
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
}, 2000);
|
|
|
|
} catch (error) {
|
|
console.error('AI generation failed:', error);
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
alert('AI analyse fejlede. Prøv manuel indtastning.');
|
|
}
|
|
}
|
|
|
|
async function saveTemplate() {
|
|
const vendorId = document.getElementById('vendorSelect').value;
|
|
const templateName = document.getElementById('templateName').value;
|
|
const productCategory = document.getElementById('productCategory').value;
|
|
|
|
console.log('Saving template...', { vendorId, templateName, productCategory, editingTemplateId });
|
|
console.log('Detection patterns:', detectionPatterns);
|
|
console.log('Field patterns:', fieldPatterns);
|
|
|
|
if (!vendorId || !templateName) {
|
|
alert('Vælg leverandør og angiv template navn');
|
|
return;
|
|
}
|
|
|
|
if (!productCategory) {
|
|
alert('Vælg produktkategori');
|
|
return;
|
|
}
|
|
|
|
if (detectionPatterns.length === 0) {
|
|
alert('Tilføj mindst ét detektionsmønster');
|
|
return;
|
|
}
|
|
|
|
// Build detection patterns from array
|
|
const detectionPatternsData = detectionPatterns.map(item => {
|
|
// Handle both string format (new) and object format (loaded from DB)
|
|
if (typeof item === 'string') {
|
|
return { type: 'text', pattern: item.trim(), weight: 0.5 };
|
|
} else {
|
|
return { type: item.type || 'text', pattern: item.pattern, weight: item.weight || 0.5 };
|
|
}
|
|
});
|
|
|
|
// Build field mappings from stored patterns
|
|
const fieldMappings = {};
|
|
|
|
if (fieldPatterns.invoice_number) {
|
|
fieldMappings.invoice_number = {
|
|
pattern: fieldPatterns.invoice_number.pattern,
|
|
group: 1
|
|
};
|
|
}
|
|
|
|
if (fieldPatterns.invoice_date) {
|
|
fieldMappings.invoice_date = {
|
|
pattern: fieldPatterns.invoice_date.pattern,
|
|
format: 'DD/MM-YY',
|
|
group: 1
|
|
};
|
|
}
|
|
|
|
if (fieldPatterns.total_amount) {
|
|
fieldMappings.total_amount = {
|
|
pattern: fieldPatterns.total_amount.pattern,
|
|
group: 1
|
|
};
|
|
}
|
|
|
|
if (fieldPatterns.cvr) {
|
|
fieldMappings.vendor_cvr = {
|
|
pattern: fieldPatterns.cvr.pattern,
|
|
group: 1
|
|
};
|
|
}
|
|
|
|
// Add line extraction patterns if provided
|
|
const linesStartPattern = document.getElementById('linesStartPattern').value;
|
|
const linesEndPattern = document.getElementById('linesEndPattern').value;
|
|
const lineItemPattern = document.getElementById('lineItemPattern').value;
|
|
|
|
if (linesStartPattern) {
|
|
fieldMappings.lines_start = { pattern: linesStartPattern };
|
|
}
|
|
if (linesEndPattern) {
|
|
fieldMappings.lines_end = { pattern: linesEndPattern };
|
|
}
|
|
if (lineItemPattern) {
|
|
fieldMappings.line_item = {
|
|
pattern: lineItemPattern,
|
|
fields: ['item_number', 'description', 'quantity', 'unit_price']
|
|
};
|
|
}
|
|
|
|
try {
|
|
const url = editingTemplateId
|
|
? `/api/v1/supplier-invoices/templates/${editingTemplateId}`
|
|
: '/api/v1/supplier-invoices/templates';
|
|
const method = editingTemplateId ? 'PUT' : 'POST';
|
|
|
|
console.log('Sending request:', { url, method, fieldMappings });
|
|
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
vendor_id: parseInt(vendorId),
|
|
template_name: templateName,
|
|
default_product_category: productCategory,
|
|
detection_patterns: detectionPatternsData,
|
|
field_mappings: fieldMappings
|
|
})
|
|
});
|
|
|
|
console.log('Response status:', response.status);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const message = editingTemplateId
|
|
? `✅ Template opdateret!\n\nÆndringerne er gemt.`
|
|
: `✅ Template gemt! ID: ${data.template_id}\n\nDu kan nu uploade fakturaer og systemet vil automatisk udtrække data.`;
|
|
alert(message);
|
|
window.location.href = '/billing/templates';
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`❌ Fejl: ${error.detail}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save template:', error);
|
|
alert('Kunne ikke gemme template');
|
|
}
|
|
}
|
|
|
|
async function testTemplate() {
|
|
const vendorId = document.getElementById('vendorSelect').value;
|
|
const templateName = document.getElementById('templateName').value;
|
|
|
|
if (!pdfText) {
|
|
alert('Ingen PDF tekst tilgængelig');
|
|
return;
|
|
}
|
|
|
|
// Build detection patterns from array
|
|
const detectionPatternsData = detectionPatterns.map(item => {
|
|
// Handle both string format (new) and object format (loaded from DB)
|
|
if (typeof item === 'string') {
|
|
return { type: 'text', pattern: item.trim(), weight: 0.5 };
|
|
} else {
|
|
return { type: item.type || 'text', pattern: item.pattern, weight: item.weight || 0.5 };
|
|
}
|
|
});
|
|
|
|
// Build field mappings - use existing fieldPatterns if loaded from DB
|
|
let fieldMappings = {};
|
|
|
|
console.log('fieldPatterns:', fieldPatterns);
|
|
console.log('Has invoice_number?', fieldPatterns.invoice_number);
|
|
console.log('Has pattern?', fieldPatterns.invoice_number?.pattern);
|
|
|
|
// If fieldPatterns already has the right structure (loaded from DB), use it directly
|
|
if (fieldPatterns.invoice_number && fieldPatterns.invoice_number.pattern) {
|
|
console.log('Using fieldPatterns directly from DB');
|
|
fieldMappings = { ...fieldPatterns };
|
|
console.log('fieldMappings after copy:', fieldMappings);
|
|
} else {
|
|
console.log('Building fieldMappings from form');
|
|
// Build from form fields (new template creation)
|
|
if (fieldPatterns.invoice_number) {
|
|
fieldMappings.invoice_number = {
|
|
pattern: fieldPatterns.invoice_number.pattern,
|
|
group: 1
|
|
};
|
|
}
|
|
|
|
if (fieldPatterns.invoice_date) {
|
|
fieldMappings.invoice_date = {
|
|
pattern: fieldPatterns.invoice_date.pattern,
|
|
format: 'DD/MM-YY',
|
|
group: 1
|
|
};
|
|
}
|
|
|
|
if (fieldPatterns.total_amount) {
|
|
fieldMappings.total_amount = {
|
|
pattern: fieldPatterns.total_amount.pattern,
|
|
group: 1
|
|
};
|
|
}
|
|
|
|
if (fieldPatterns.vendor_cvr || fieldPatterns.cvr) {
|
|
fieldMappings.vendor_cvr = {
|
|
pattern: (fieldPatterns.vendor_cvr || fieldPatterns.cvr).pattern,
|
|
group: 1
|
|
};
|
|
}
|
|
}
|
|
|
|
// Add line extraction patterns from form inputs
|
|
const linesStartPattern = document.getElementById('linesStartPattern').value;
|
|
const linesEndPattern = document.getElementById('linesEndPattern').value;
|
|
const lineItemPattern = document.getElementById('lineItemPattern').value;
|
|
|
|
if (linesStartPattern) {
|
|
fieldMappings.lines_start = { pattern: linesStartPattern };
|
|
}
|
|
if (linesEndPattern) {
|
|
fieldMappings.lines_end = { pattern: linesEndPattern };
|
|
}
|
|
if (lineItemPattern) {
|
|
fieldMappings.line_item = {
|
|
pattern: lineItemPattern,
|
|
fields: ['item_number', 'description', 'quantity', 'unit_price']
|
|
};
|
|
}
|
|
|
|
// Simulate template test (create temp template or test locally)
|
|
try {
|
|
const testResults = document.getElementById('testResults');
|
|
testResults.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
|
|
testResults.innerHTML = '<div class="spinner-border spinner-border-sm me-2"></div>Tester template...';
|
|
testResults.classList.add('alert-info');
|
|
|
|
// Test detection patterns locally
|
|
let totalScore = 0;
|
|
let maxScore = 0;
|
|
let detectionHtml = '<h6>Detektionsmønstre:</h6><ul class="mb-0">';
|
|
|
|
for (let dp of detectionPatternsData) {
|
|
maxScore += dp.weight;
|
|
const found = pdfText.includes(dp.pattern);
|
|
if (found) totalScore += dp.weight;
|
|
|
|
detectionHtml += `<li>${found ? '✅' : '❌'} "${dp.pattern}" (weight: ${dp.weight})</li>`;
|
|
}
|
|
detectionHtml += '</ul>';
|
|
|
|
const confidence = maxScore > 0 ? (totalScore / maxScore) : 0;
|
|
const matched = confidence >= 0.7;
|
|
|
|
// Test field extraction locally
|
|
let extractedHtml = '<h6 class="mt-3">Udtrækkede felter:</h6><ul class="mb-0">';
|
|
let extractedCount = 0;
|
|
|
|
for (let [fieldName, config] of Object.entries(fieldMappings)) {
|
|
if (['lines_start', 'lines_end', 'line_item'].includes(fieldName)) continue;
|
|
|
|
console.log(`Testing field ${fieldName}:`, config);
|
|
|
|
try {
|
|
const regex = new RegExp(config.pattern, 'i');
|
|
console.log(`Regex for ${fieldName}:`, regex);
|
|
|
|
const match = pdfText.match(regex);
|
|
console.log(`Match result for ${fieldName}:`, match);
|
|
|
|
if (match && match[config.group]) {
|
|
extractedHtml += `<li>✅ <strong>${fieldName}:</strong> "${match[config.group].trim()}"</li>`;
|
|
extractedCount++;
|
|
} else {
|
|
extractedHtml += `<li>❌ <strong>${fieldName}:</strong> Ikke fundet</li>`;
|
|
}
|
|
} catch (e) {
|
|
extractedHtml += `<li>⚠️ <strong>${fieldName}:</strong> Pattern fejl - ${e.message}</li>`;
|
|
}
|
|
}
|
|
extractedHtml += '</ul>';
|
|
|
|
// Test line item extraction
|
|
let lineItemsHtml = '';
|
|
if (fieldMappings.lines_start && fieldMappings.lines_end && fieldMappings.line_item) {
|
|
lineItemsHtml = '<h6 class="mt-3">Varelinjer:</h6>';
|
|
|
|
try {
|
|
const startMatch = pdfText.match(new RegExp(fieldMappings.lines_start.pattern, 'i'));
|
|
const endMatch = pdfText.match(new RegExp(fieldMappings.lines_end.pattern, 'i'));
|
|
|
|
if (startMatch && endMatch) {
|
|
const startPos = pdfText.indexOf(startMatch[0]) + startMatch[0].length;
|
|
const endPos = pdfText.indexOf(endMatch[0]);
|
|
const lineSection = pdfText.substring(startPos, endPos);
|
|
|
|
// Check if we have separate item and price patterns (ALSO style)
|
|
if (fieldMappings.line_price) {
|
|
// Two-pattern extraction: item info + price info
|
|
const itemRegex = new RegExp(fieldMappings.line_item.pattern, 'gim');
|
|
const priceRegex = new RegExp(fieldMappings.line_price.pattern, 'gim');
|
|
|
|
const itemMatches = [...lineSection.matchAll(itemRegex)];
|
|
const priceMatches = [...lineSection.matchAll(priceRegex)];
|
|
|
|
console.log('Item matches:', itemMatches.length, 'Price matches:', priceMatches.length);
|
|
|
|
if (itemMatches.length > 0 && priceMatches.length > 0) {
|
|
lineItemsHtml += `<p class="mb-2">✅ Fandt ${Math.min(itemMatches.length, priceMatches.length)} varelinjer:</p>`;
|
|
lineItemsHtml += '<div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr>';
|
|
lineItemsHtml += '<th>Position</th><th>Item</th><th>Description</th><th>Qty</th><th>Price</th><th>Total</th><th>VAT</th>';
|
|
lineItemsHtml += '</tr></thead><tbody>';
|
|
|
|
// Combine item and price matches
|
|
const maxLines = Math.min(5, itemMatches.length, priceMatches.length);
|
|
for (let i = 0; i < maxLines; i++) {
|
|
const item = itemMatches[i];
|
|
const price = priceMatches[i];
|
|
|
|
// Check for VAT markers between this price and next item
|
|
const priceEndPos = price.index + price[0].length;
|
|
let nextItemStartPos = lineSection.length;
|
|
|
|
// Find start of next item (if exists)
|
|
if (i + 1 < itemMatches.length) {
|
|
nextItemStartPos = itemMatches[i + 1].index;
|
|
}
|
|
|
|
// Check section between price and next item
|
|
const betweenSection = lineSection.substring(priceEndPos, nextItemStartPos);
|
|
|
|
console.log(`Item ${i} (pos ${item[1]}):`, {
|
|
priceEndPos,
|
|
nextItemStartPos,
|
|
betweenLength: betweenSection.length,
|
|
betweenPreview: betweenSection.substring(0, 100)
|
|
});
|
|
|
|
const hasReverseCharge = /omvendt.*betalingspligt/i.test(betweenSection);
|
|
const hasCopydan = /copydan/i.test(betweenSection);
|
|
|
|
console.log(` VAT checks: Omvendt=${hasReverseCharge}, Copydan=${hasCopydan}`);
|
|
|
|
let vatMarker = '';
|
|
if (hasReverseCharge && hasCopydan) {
|
|
vatMarker = '<span class="badge bg-warning text-dark">Omvendt</span> <span class="badge bg-info">Copydan</span>';
|
|
} else if (hasReverseCharge) {
|
|
vatMarker = '<span class="badge bg-warning text-dark">Omvendt</span>';
|
|
} else if (hasCopydan) {
|
|
vatMarker = '<span class="badge bg-info">Copydan</span>';
|
|
}
|
|
|
|
lineItemsHtml += '<tr>';
|
|
lineItemsHtml += `<td>${item[1]}</td>`; // position
|
|
lineItemsHtml += `<td>${item[2]}</td>`; // item_number
|
|
lineItemsHtml += `<td>${item[3] ? item[3].trim().substring(0, 40) : ''}</td>`; // description (truncated)
|
|
lineItemsHtml += `<td>${price[1]}</td>`; // quantity
|
|
lineItemsHtml += `<td>${price[2]}</td>`; // unit_price
|
|
lineItemsHtml += `<td>${price[3]}</td>`; // total_price
|
|
lineItemsHtml += `<td>${vatMarker}</td>`; // vat marker
|
|
lineItemsHtml += '</tr>';
|
|
}
|
|
|
|
lineItemsHtml += '</tbody></table></div>';
|
|
const totalLines = Math.min(itemMatches.length, priceMatches.length);
|
|
if (totalLines > 5) {
|
|
lineItemsHtml += `<p class="text-muted"><small>... og ${totalLines - 5} linjer mere</small></p>`;
|
|
}
|
|
} else {
|
|
lineItemsHtml += `<p class="text-warning">❌ Fandt ${itemMatches.length} item-linjer og ${priceMatches.length} pris-linjer</p>`;
|
|
}
|
|
} else {
|
|
// Single-pattern extraction (old style)
|
|
const lineRegex = new RegExp(fieldMappings.line_item.pattern, 'gim');
|
|
const lines = [...lineSection.matchAll(lineRegex)];
|
|
|
|
if (lines.length > 0) {
|
|
lineItemsHtml += `<p class="mb-2">✅ Fandt ${lines.length} varelinjer:</p>`;
|
|
lineItemsHtml += '<div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr>';
|
|
|
|
const fields = fieldMappings.line_item.fields || ['position', 'item_number', 'description', 'quantity', 'unit_price', 'total_price'];
|
|
fields.forEach(f => {
|
|
lineItemsHtml += `<th>${f}</th>`;
|
|
});
|
|
lineItemsHtml += '</tr></thead><tbody>';
|
|
|
|
// Show first 5 lines
|
|
lines.slice(0, 5).forEach(match => {
|
|
lineItemsHtml += '<tr>';
|
|
for (let i = 1; i <= fields.length; i++) {
|
|
lineItemsHtml += `<td>${match[i] ? match[i].trim() : ''}</td>`;
|
|
}
|
|
lineItemsHtml += '</tr>';
|
|
});
|
|
|
|
lineItemsHtml += '</tbody></table></div>';
|
|
if (lines.length > 5) {
|
|
lineItemsHtml += `<p class="text-muted"><small>... og ${lines.length - 5} linjer mere</small></p>`;
|
|
}
|
|
} else {
|
|
lineItemsHtml += '<p class="text-warning">❌ Ingen linjer fundet med pattern</p>';
|
|
}
|
|
}
|
|
} else {
|
|
lineItemsHtml += `<p class="text-warning">⚠️ Start eller slut marker ikke fundet</p>`;
|
|
if (!startMatch) lineItemsHtml += `<small>Start pattern: "${fieldMappings.lines_start.pattern}" ikke fundet</small><br>`;
|
|
if (!endMatch) lineItemsHtml += `<small>Slut pattern: "${fieldMappings.lines_end.pattern}" ikke fundet</small>`;
|
|
}
|
|
} catch (e) {
|
|
lineItemsHtml += `<p class="text-danger">❌ Fejl: ${e.message}</p>`;
|
|
console.error('Line extraction error:', e);
|
|
}
|
|
}
|
|
|
|
// Show results
|
|
testResults.innerHTML = `
|
|
<h5>${matched ? '✅' : '❌'} Template ${matched ? 'MATCHER' : 'MATCHER IKKE'}</h5>
|
|
<p><strong>Confidence:</strong> ${(confidence * 100).toFixed(0)}% (threshold: 70%)</p>
|
|
${detectionHtml}
|
|
${extractedHtml}
|
|
${lineItemsHtml}
|
|
`;
|
|
|
|
if (matched && extractedCount > 0) {
|
|
testResults.classList.remove('alert-info');
|
|
testResults.classList.add('alert-success');
|
|
} else if (matched) {
|
|
testResults.classList.remove('alert-info');
|
|
testResults.classList.add('alert-warning');
|
|
} else {
|
|
testResults.classList.remove('alert-info');
|
|
testResults.classList.add('alert-danger');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Test failed:', error);
|
|
const testResults = document.getElementById('testResults');
|
|
testResults.classList.remove('d-none', 'alert-info');
|
|
testResults.classList.add('alert-danger');
|
|
testResults.innerHTML = `<strong>Test fejlede:</strong> ${error.message}`;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|