bmc_hub/app/billing/frontend/template_builder.html
Christian dcb4d8a280 feat: Implement supplier invoices management with e-conomic integration
- Added FastAPI views for supplier invoices in the billing frontend.
- Created EconomicService for handling e-conomic API interactions, including safety modes for read-only and dry-run operations.
- Developed database migration for supplier invoices, including tables for invoices, line items, and settings.
- Documented kassekladde module features, architecture, API endpoints, and usage guide in KASSEKLADDE.md.
- Implemented views for overdue invoices and pending e-conomic sync.
2025-12-07 03:29:54 +01:00

1262 lines
54 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>
<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 = {};
// Load pending files on page load
document.addEventListener('DOMContentLoaded', async () => {
await loadPendingFiles();
await loadVendors();
});
async function loadPendingFiles() {
try {
const response = await fetch('/api/v1/supplier-invoices/pending-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;
}
// Auto-generate regex pattern based on selected text
const pattern = generatePattern(selectedText, fieldName);
// Store pattern
fieldPatterns[fieldName] = {
value: selectedText,
pattern: pattern
};
// 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) {
// Find context before the value in PDF
const index = pdfText.indexOf(text);
if (index === -1) return escapeRegex(text);
// Get 30 chars before for better context
const before = pdfText.substring(Math.max(0, index - 30), index).trim();
const words = before.split(/\s+/);
const lastWord = words[words.length - 1] || '';
const secondLastWord = words[words.length - 2] || '';
// Generate pattern based on field type
if (fieldName === 'invoice_number') {
// Number pattern
if (/^\d+$/.test(text)) {
return `${escapeRegex(lastWord)}\\s*(\\d+)`;
}
} else if (fieldName === 'invoice_date') {
// Date pattern - very flexible
// Detect various date formats: DD/MM-YY, DD-MM-YYYY, DD.MM.YYYY, etc.
const datePatterns = [
/(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{2,4})/, // DD/MM/YY or DD-MM-YYYY
/(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{2})/, // DD/MM-YY
/(\d{2,4})[\/\-\.](\d{1,2})[\/\-\.](\d{1,2})/ // YYYY-MM-DD
];
for (let dp of datePatterns) {
if (dp.test(text)) {
// Use flexible pattern that matches any separator
return `${escapeRegex(lastWord)}\\s*(\\d{1,2}[\\/.\\-]\\d{1,2}[\\/.\\-]\\d{2,4})`;
}
}
// If no pattern matches, try with two words context
if (secondLastWord) {
return `${escapeRegex(secondLastWord)}\\s+${escapeRegex(lastWord)}\\s*(.+)`;
}
} else if (fieldName === 'total_amount') {
// Amount pattern - handle Danish format (1.234,56 or 1234,56)
if (/[\d.,]+/.test(text)) {
return `${escapeRegex(lastWord)}\\s*([\\d.,]+)`;
}
} else if (fieldName === 'cvr') {
// CVR pattern
if (/\d{8}/.test(text)) {
return `${escapeRegex(lastWord)}\\s*(\\d{8})`;
}
}
// Fallback: exact match with context
return `${escapeRegex(lastWord)}\\s*(${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;
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(text => ({
type: 'text',
pattern: text.trim(),
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 response = await fetch('/api/v1/supplier-invoices/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vendor_id: parseInt(vendorId),
template_name: templateName,
detection_patterns: detectionPatternsData,
field_mappings: fieldMappings
})
});
if (response.ok) {
const data = await response.json();
alert(`✅ Template gemt! ID: ${data.template_id}\n\nDu kan nu uploade fakturaer og systemet vil automatisk udtrække data.`);
window.location.href = '/billing/supplier-invoices';
} 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(text => ({
type: 'text',
pattern: text.trim(),
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']
};
}
// 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;
try {
const regex = new RegExp(config.pattern, 'i');
const match = pdfText.match(regex);
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>';
// 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}
`;
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>