bmc_hub/app/billing/frontend/template_builder.html
Christian 890bd6245d feat: Add template editing functionality and improve file loading logic
- Added an "Edit" button for templates in the templates list, redirecting to the template builder.
- Enhanced loadPendingFiles function to filter files by vendor ID, displaying a message if no files are found.
- Modified openTestModal to load vendor-specific files based on the selected template.
- Updated Ollama model configuration for improved JSON extraction.
- Refactored Ollama service to support different API formats based on model type.
- Implemented lazy loading of templates in TemplateService for better performance.
- Added VAT note extraction for invoice line items.
- Updated Docker Compose configuration for Ollama model settings.
2025-12-08 23:46:18 +01:00

1703 lines
76 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>
</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>
<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();
}
});
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;
// 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 {
// Reprocess file to get PDF text
const response = await fetch(`/api/v1/supplier-invoices/reprocess/${fileId}`, {
method: 'POST'
});
const data = await response.json();
currentFile = {
file_id: fileId,
filename: filename,
text: data.pdf_text
};
pdfText = data.pdf_text;
// Show PDF preview
document.getElementById('pdfPreview').textContent = pdfText;
nextStep(2);
} catch (error) {
console.error('Failed to load file:', error);
alert('Kunne ikke hente fil');
}
}
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;
console.log('Saving template...', { vendorId, templateName, 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 (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,
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>