2025-12-07 03:29:54 +01:00
<!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 >
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
< 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 >
2025-12-07 03:29:54 +01:00
< / 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 >
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
< 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 >
2025-12-07 03:29:54 +01:00
< 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 >
2025-12-08 23:46:18 +01:00
<!-- 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 >
2025-12-07 03:29:54 +01:00
< 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 = {};
2025-12-08 23:46:18 +01:00
let editingTemplateId = null; // Track if we're editing
2025-12-07 03:29:54 +01:00
// Load pending files on page load
document.addEventListener('DOMContentLoaded', async () => {
2025-12-08 23:46:18 +01:00
// 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();
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
// 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
}
2025-12-08 23:46:18 +01:00
}
2025-12-07 03:29:54 +01:00
});
2025-12-08 23:46:18 +01:00
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;
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
// Set product category
if (template.default_product_category) {
document.getElementById('productCategory').value = template.default_product_category;
}
2025-12-08 23:46:18 +01:00
// 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');
}
2025-12-07 03:29:54 +01:00
async function loadPendingFiles() {
try {
2025-12-08 23:46:18 +01:00
const response = await fetch('/api/v1/pending-supplier-invoice-files');
2025-12-07 03:29:54 +01:00
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 {
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
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}`);
}
2025-12-07 03:29:54 +01:00
const data = await response.json();
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
console.log('📦 Response data:', data);
if (!data.pdf_text) {
console.warn('⚠️ No PDF text in response');
}
2025-12-07 03:29:54 +01:00
currentFile = {
file_id: fileId,
filename: filename,
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
text: data.pdf_text || ''
2025-12-07 03:29:54 +01:00
};
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
pdfText = data.pdf_text || '';
console.log(`✅ File loaded, PDF text length: ${pdfText.length} chars`);
2025-12-07 03:29:54 +01:00
// Show PDF preview
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
const pdfPreview = document.getElementById('pdfPreview');
if (pdfPreview) {
pdfPreview.textContent = pdfText;
}
2025-12-07 03:29:54 +01:00
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
console.log('🚀 Advancing to step 2');
2025-12-07 03:29:54 +01:00
nextStep(2);
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
2025-12-07 03:29:54 +01:00
} catch (error) {
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
console.error('❌ Failed to load file:', error);
alert('Kunne ikke hente fil: ' + error.message);
2025-12-07 03:29:54 +01:00
}
}
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
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);
}
2025-12-07 03:29:54 +01:00
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;
}
2025-12-08 23:46:18 +01:00
console.log('setField called:', { fieldName, selectedText });
2025-12-07 03:29:54 +01:00
// Auto-generate regex pattern based on selected text
const pattern = generatePattern(selectedText, fieldName);
2025-12-08 23:46:18 +01:00
console.log('Generated pattern:', pattern);
2025-12-07 03:29:54 +01:00
// Store pattern
fieldPatterns[fieldName] = {
value: selectedText,
pattern: pattern
};
2025-12-08 23:46:18 +01:00
console.log('Stored in fieldPatterns:', fieldPatterns[fieldName]);
2025-12-07 03:29:54 +01:00
// 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) {
2025-12-08 23:46:18 +01:00
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);
2025-12-07 03:29:54 +01:00
2025-12-08 23:46:18 +01:00
let label = '';
let value = '';
2025-12-07 03:29:54 +01:00
2025-12-08 23:46:18 +01:00
// For invoice_number, date, amount: first word is usually the label
2025-12-07 03:29:54 +01:00
if (fieldName === 'invoice_number') {
2025-12-08 23:46:18 +01:00
// 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!');
2025-12-07 03:29:54 +01:00
}
} else if (fieldName === 'invoice_date') {
2025-12-08 23:46:18 +01:00
// 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;
2025-12-07 03:29:54 +01:00
}
} else if (fieldName === 'total_amount') {
2025-12-08 23:46:18 +01:00
// 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;
2025-12-07 03:29:54 +01:00
}
} else if (fieldName === 'cvr') {
2025-12-08 23:46:18 +01:00
// 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;
2025-12-07 03:29:54 +01:00
}
}
2025-12-08 23:46:18 +01:00
// 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);
2025-12-07 03:29:54 +01:00
}
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;
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
const productCategory = document.getElementById('productCategory').value;
2025-12-07 03:29:54 +01:00
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
console.log('Saving template...', { vendorId, templateName, productCategory, editingTemplateId });
2025-12-08 23:46:18 +01:00
console.log('Detection patterns:', detectionPatterns);
console.log('Field patterns:', fieldPatterns);
2025-12-07 03:29:54 +01:00
if (!vendorId || !templateName) {
alert('Vælg leverandør og angiv template navn');
return;
}
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
if (!productCategory) {
alert('Vælg produktkategori');
return;
}
2025-12-07 03:29:54 +01:00
if (detectionPatterns.length === 0) {
alert('Tilføj mindst ét detektionsmønster');
return;
}
// Build detection patterns from array
2025-12-08 23:46:18 +01:00
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 };
}
});
2025-12-07 03:29:54 +01:00
// 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 {
2025-12-08 23:46:18 +01:00
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,
2025-12-07 03:29:54 +01:00
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vendor_id: parseInt(vendorId),
template_name: templateName,
feat: Implement quick analysis on PDF upload for CVR, document type, and number extraction
- 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.
2025-12-09 14:54:33 +01:00
default_product_category: productCategory,
2025-12-07 03:29:54 +01:00
detection_patterns: detectionPatternsData,
field_mappings: fieldMappings
})
});
2025-12-08 23:46:18 +01:00
console.log('Response status:', response.status);
2025-12-07 03:29:54 +01:00
if (response.ok) {
const data = await response.json();
2025-12-08 23:46:18 +01:00
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';
2025-12-07 03:29:54 +01:00
} 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
2025-12-08 23:46:18 +01:00
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 };
}
});
2025-12-07 03:29:54 +01:00
2025-12-08 23:46:18 +01:00
// Build field mappings - use existing fieldPatterns if loaded from DB
let fieldMappings = {};
2025-12-07 03:29:54 +01:00
2025-12-08 23:46:18 +01:00
console.log('fieldPatterns:', fieldPatterns);
console.log('Has invoice_number?', fieldPatterns.invoice_number);
console.log('Has pattern?', fieldPatterns.invoice_number?.pattern);
2025-12-07 03:29:54 +01:00
2025-12-08 23:46:18 +01:00
// 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
};
}
2025-12-07 03:29:54 +01:00
}
2025-12-08 23:46:18 +01:00
// Add line extraction patterns from form inputs
2025-12-07 03:29:54 +01:00
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;
2025-12-08 23:46:18 +01:00
console.log(`Testing field ${fieldName}:`, config);
2025-12-07 03:29:54 +01:00
try {
const regex = new RegExp(config.pattern, 'i');
2025-12-08 23:46:18 +01:00
console.log(`Regex for ${fieldName}:`, regex);
2025-12-07 03:29:54 +01:00
const match = pdfText.match(regex);
2025-12-08 23:46:18 +01:00
console.log(`Match result for ${fieldName}:`, match);
2025-12-07 03:29:54 +01:00
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 > ';
2025-12-08 23:46:18 +01:00
// 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);
}
}
2025-12-07 03:29:54 +01:00
// 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}
2025-12-08 23:46:18 +01:00
${lineItemsHtml}
2025-12-07 03:29:54 +01:00
`;
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 >