- Added customer_id and contact_id filters to the list_opportunities endpoint for improved querying. - Implemented a redirect for opportunity detail pages to a new format. - Refactored SubscriptionMatrixService to load invoices from a local snapshot instead of an external service, improving performance and reliability. - Updated settings to include 'pipeline' as a case type and added a new section for managing ignored product texts in the invoice error finder. - Introduced a new user_sag_create_preferences table to store per-user default case types for new cases. - Enhanced frontend settings page with invoice error finder configuration options and improved handling of ignored product texts. - Added migrations to support new features, including resolved status for invoice error finder issues and user-specific case type preferences.
568 lines
24 KiB
HTML
568 lines
24 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Faktura-fejl-finder - Fejlliste - BMC Hub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">📋 Faktura-fejl-liste</h1>
|
|
<p class="text-muted mb-0">Gennemgå, godkend og håndter registrerede fakturaafvigelser.</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="/invoice-error-finder" class="btn btn-outline-secondary">
|
|
<i class="bi bi-arrow-left me-1"></i>Tilbage til dashboard
|
|
</a>
|
|
<button class="btn btn-primary" onclick="loadIssues()">
|
|
<i class="bi bi-arrow-clockwise me-1"></i>Opdater
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card border-0 shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">Status</label>
|
|
<select id="filterStatus" class="form-select" onchange="loadIssues()">
|
|
<option value="">Alle</option>
|
|
<option value="open" selected>Åben</option>
|
|
<option value="investigating">Under undersøgelse</option>
|
|
<option value="approved_change">Godkendt ændring</option>
|
|
<option value="error_found">Fejl fundet</option>
|
|
<option value="ready_to_invoice">Opret ordrekladde</option>
|
|
<option value="invoiced">Faktureret</option>
|
|
<option value="ignored">Ignoreret</option>
|
|
<option value="resolved">Løst</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">Fejltype</label>
|
|
<select id="filterType" class="form-select" onchange="loadIssues()">
|
|
<option value="">Alle</option>
|
|
<option value="missing_line">Manglende varelinje</option>
|
|
<option value="open_order_not_invoiced">Åben salgsordre ikke faktureret</option>
|
|
<option value="quantity_drop">Antalsfald</option>
|
|
<option value="price_change">Prisændring</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">Kunde</label>
|
|
<select id="filterCustomer" class="form-select" onchange="loadIssues()">
|
|
<option value="">Alle</option>
|
|
{% for customer in customers %}
|
|
<option value="{{ customer.id }}">{{ customer.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">Ansvarlig</label>
|
|
<select id="filterAssigned" class="form-select" onchange="loadIssues()">
|
|
<option value="">Alle</option>
|
|
<option value="null">Ikke tildelt</option>
|
|
{% for user in users %}
|
|
<option value="{{ user.user_id }}">{{ user.display_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="issuesStatus" class="alert d-none mb-4"></div>
|
|
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Kunde</th>
|
|
<th>Fejltype</th>
|
|
<th>Vare</th>
|
|
<th>Forventet</th>
|
|
<th>Faktisk</th>
|
|
<th>Periode</th>
|
|
<th>Beløb/impact</th>
|
|
<th>Status</th>
|
|
<th>Ansvarlig</th>
|
|
<th style="min-width: 220px;">Handling</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="issuesBody">
|
|
<tr><td colspan="10" class="text-muted text-center py-4">Indlæser...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
|
<span class="text-muted small" id="paginationInfo"></span>
|
|
<div class="btn-group" id="paginationControls"></div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="historyModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-scrollable" style="max-width: 90vw; width: 90vw;">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<div>
|
|
<h5 class="modal-title mb-1">Fakturahistorik</h5>
|
|
<div class="text-muted small" id="historyModalMeta"></div>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="historyModalStatus" class="alert d-none mb-3"></div>
|
|
<div class="d-flex flex-wrap gap-2 mb-3" id="historyModalActions">
|
|
<button type="button" class="btn btn-outline-success btn-sm" id="historyApproveBtn" disabled>
|
|
<i class="bi bi-check2 me-1"></i>Godkend
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" id="historyInvestigatingBtn" disabled>
|
|
<i class="bi bi-search me-1"></i>Undersøgelse
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="historyReadyBtn" disabled>
|
|
<i class="bi bi-receipt me-1"></i>Opret ordrekladde
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" id="historyCreateSagBtn" disabled>
|
|
<i class="bi bi-folder-plus me-1"></i>Opret sag
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm align-middle">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Måned</th>
|
|
<th>Status</th>
|
|
<th>Antal linjer</th>
|
|
<th>Samlet mængde</th>
|
|
<th>Samlet beløb</th>
|
|
<th>Fakturaer</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="historyModalBody">
|
|
<tr><td colspan="6" class="text-muted text-center py-4">Vælg en fejl for at se historik</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentOffset = 0;
|
|
const pageSize = 100;
|
|
let currentHistoryIssueId = null;
|
|
|
|
const issueTypeLabels = {
|
|
missing_line: 'Manglende varelinje',
|
|
open_order_not_invoiced: 'Åben salgsordre ikke faktureret',
|
|
quantity_drop: 'Antalsfald',
|
|
price_change: 'Prisændring',
|
|
new_item_never_invoiced: 'Ny vare aldrig faktureret'
|
|
};
|
|
|
|
const statusLabels = {
|
|
open: 'Åben',
|
|
investigating: 'Under undersøgelse',
|
|
approved_change: 'Godkendt ændring',
|
|
error_found: 'Fejl fundet',
|
|
ready_to_invoice: 'Opret ordrekladde',
|
|
invoiced: 'Faktureret',
|
|
ignored: 'Ignoreret',
|
|
resolved: 'Løst'
|
|
};
|
|
|
|
function escapeHtml(text) {
|
|
if (text == null) return '';
|
|
return String(text)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"');
|
|
}
|
|
|
|
function formatCurrency(value) {
|
|
if (value == null) return '-';
|
|
return new Intl.NumberFormat('da-DK', { style: 'currency', currency: 'DKK' }).format(value);
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
if (value == null) return '-';
|
|
return new Intl.NumberFormat('da-DK').format(value);
|
|
}
|
|
|
|
function formatMonth(value) {
|
|
if (!value) return '-';
|
|
return new Intl.DateTimeFormat('da-DK', { year: 'numeric', month: 'short' }).format(new Date(value));
|
|
}
|
|
|
|
function statusBadge(status) {
|
|
const map = {
|
|
open: 'bg-danger',
|
|
investigating: 'bg-warning text-dark',
|
|
approved_change: 'bg-info text-dark',
|
|
error_found: 'bg-danger',
|
|
ready_to_invoice: 'bg-success',
|
|
invoiced: 'bg-secondary',
|
|
ignored: 'bg-light text-dark',
|
|
resolved: 'bg-secondary-subtle text-dark'
|
|
};
|
|
const cls = map[status] || 'bg-light text-dark';
|
|
return `<span class="badge ${cls}">${statusLabels[status] || status}</span>`;
|
|
}
|
|
|
|
function buildQueryParams() {
|
|
const params = new URLSearchParams();
|
|
params.set('limit', pageSize);
|
|
params.set('offset', currentOffset);
|
|
|
|
const status = document.getElementById('filterStatus').value;
|
|
if (status) params.set('status', status);
|
|
|
|
const type = document.getElementById('filterType').value;
|
|
if (type) params.set('issue_type', type);
|
|
|
|
const customer = document.getElementById('filterCustomer').value;
|
|
if (customer) params.set('customer_id', customer);
|
|
|
|
const assigned = document.getElementById('filterAssigned').value;
|
|
if (assigned === 'null') {
|
|
params.set('assigned_user_id', 'null');
|
|
} else if (assigned) {
|
|
params.set('assigned_user_id', assigned);
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
async function loadIssues() {
|
|
const body = document.getElementById('issuesBody');
|
|
body.innerHTML = '<tr><td colspan="10" class="text-muted text-center py-4">Indlæser...</td></tr>';
|
|
|
|
try {
|
|
const params = buildQueryParams();
|
|
const res = await fetch('/api/v1/invoice-error-finder/issues?' + params.toString());
|
|
if (!res.ok) throw new Error('Kunne ikke hente fejlliste');
|
|
const data = await res.json();
|
|
|
|
if (data.items.length === 0) {
|
|
body.innerHTML = '<tr><td colspan="10" class="text-muted text-center py-4">Ingen fejl fundet</td></tr>';
|
|
} else {
|
|
body.innerHTML = data.items.map(issue => `
|
|
<tr>
|
|
<td>${escapeHtml(issue.customer_name || 'Ukendt kunde')}</td>
|
|
<td>${issueTypeLabels[issue.issue_type] || issue.issue_type}</td>
|
|
<td>${escapeHtml(issue.resolved_product_name || issue.product_name || issue.product_number || '-')}</td>
|
|
<td>${formatNumber(issue.expected_quantity ?? issue.expected_price)}</td>
|
|
<td>${formatNumber(issue.actual_quantity ?? issue.actual_price)}</td>
|
|
<td>${issue.reference_period_start || '-'}</td>
|
|
<td>${formatCurrency(issue.amount_impact)}</td>
|
|
<td>${statusBadge(issue.status)}</td>
|
|
<td>${escapeHtml(issue.assigned_user_name || '-')}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
${renderActionButtons(issue)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
document.getElementById('paginationInfo').textContent = `Viser ${data.items.length} af ${data.total} fejl`;
|
|
renderPagination(data.total);
|
|
} catch (err) {
|
|
body.innerHTML = `<tr><td colspan="10" class="text-danger text-center py-4">Fejl: ${escapeHtml(err.message)}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
function renderActionButtons(issue) {
|
|
const historyButton = `<button class="btn btn-outline-dark" title="Se 13 måneder tilbage og 2 frem" onclick="showInvoiceHistory(${issue.id})"><i class="bi bi-clock-history"></i></button>`;
|
|
if (issue.status === 'ignored') return `${historyButton}<span class="text-muted small ms-2">Ignoreret</span>`;
|
|
if (issue.status === 'invoiced') return `${historyButton}<span class="text-muted small ms-2">Faktureret</span>`;
|
|
if (issue.status === 'resolved') return `${historyButton}<span class="text-muted small ms-2">Løst</span>`;
|
|
|
|
return `
|
|
${historyButton}
|
|
<button class="btn btn-outline-success" title="Godkend" onclick="updateStatus(${issue.id}, 'approved_change')"><i class="bi bi-check"></i></button>
|
|
<button class="btn btn-outline-warning" title="Under undersøgelse" onclick="updateStatus(${issue.id}, 'investigating')"><i class="bi bi-search"></i></button>
|
|
<button class="btn btn-outline-primary" title="Opret ordrekladde" onclick="createOrdreDraft(${issue.id})"><i class="bi bi-receipt"></i></button>
|
|
<button class="btn btn-outline-info" title="Opret sag" onclick="createSag(${issue.id})"><i class="bi bi-folder-plus"></i></button>
|
|
<button class="btn btn-outline-secondary" title="Ignorér" onclick="ignoreIssue(${issue.id})"><i class="bi bi-eye-slash"></i></button>
|
|
`;
|
|
}
|
|
|
|
function renderPagination(total) {
|
|
const controls = document.getElementById('paginationControls');
|
|
const pages = Math.ceil(total / pageSize);
|
|
if (pages <= 1) {
|
|
controls.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const currentPage = Math.floor(currentOffset / pageSize);
|
|
let html = `<button class="btn btn-outline-secondary" ${currentOffset === 0 ? 'disabled' : ''} onclick="goToPage(0)">«</button>`;
|
|
for (let i = 0; i < pages; i++) {
|
|
const active = i === currentPage ? 'active' : '';
|
|
html += `<button class="btn btn-outline-secondary ${active}" onclick="goToPage(${i})">${i + 1}</button>`;
|
|
}
|
|
html += `<button class="btn btn-outline-secondary" ${currentOffset + pageSize >= total ? 'disabled' : ''} onclick="goToPage(${pages - 1})">»</button>`;
|
|
controls.innerHTML = html;
|
|
}
|
|
|
|
function goToPage(page) {
|
|
currentOffset = page * pageSize;
|
|
loadIssues();
|
|
}
|
|
|
|
async function updateStatus(issueId, status, options = {}) {
|
|
const { target = 'page', successMessage = 'Status opdateret' } = options;
|
|
try {
|
|
const res = await fetch(`/api/v1/invoice-error-finder/issues/${issueId}/status`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status })
|
|
});
|
|
if (!res.ok) throw new Error('Opdatering fejlede');
|
|
if (target === 'history') {
|
|
showHistoryStatus(successMessage, 'success');
|
|
} else {
|
|
showStatus(successMessage, 'success');
|
|
}
|
|
loadIssues();
|
|
} catch (err) {
|
|
if (target === 'history') {
|
|
showHistoryStatus('Fejl: ' + err.message, 'danger');
|
|
} else {
|
|
showStatus('Fejl: ' + err.message, 'danger');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createSag(issueId, options = {}) {
|
|
const { target = 'page' } = options;
|
|
const titel = prompt('Titel på sag:');
|
|
if (!titel) return;
|
|
try {
|
|
const res = await fetch(`/api/v1/invoice-error-finder/issues/${issueId}/create-sag`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ titel })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Opret sag fejlede');
|
|
if (target === 'history') {
|
|
showHistoryStatus(`Sag #${data.sag_id} oprettet`, 'success');
|
|
} else {
|
|
showStatus(`Sag #${data.sag_id} oprettet`, 'success');
|
|
}
|
|
loadIssues();
|
|
} catch (err) {
|
|
if (target === 'history') {
|
|
showHistoryStatus('Fejl: ' + err.message, 'danger');
|
|
} else {
|
|
showStatus('Fejl: ' + err.message, 'danger');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createOrdreDraft(issueId) {
|
|
try {
|
|
const res = await fetch(`/api/v1/invoice-error-finder/issues/${issueId}/create-ordre-draft`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({})
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Opret kladde fejlede');
|
|
showStatus(`Ordrekladde #${data.draft_id} oprettet`, 'success');
|
|
loadIssues();
|
|
} catch (err) {
|
|
showStatus('Fejl: ' + err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function ignoreIssue(issueId) {
|
|
if (!confirm('Ignorér denne fejl?')) return;
|
|
try {
|
|
const res = await fetch(`/api/v1/invoice-error-finder/issues/${issueId}/ignore`, {
|
|
method: 'POST'
|
|
});
|
|
if (!res.ok) throw new Error('Ignorér fejlede');
|
|
showStatus('Fejl ignoreret', 'success');
|
|
loadIssues();
|
|
} catch (err) {
|
|
showStatus('Fejl: ' + err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
function showStatus(message, type) {
|
|
const el = document.getElementById('issuesStatus');
|
|
el.className = `alert alert-${type} mb-4`;
|
|
el.textContent = message;
|
|
el.classList.remove('d-none');
|
|
setTimeout(() => el.classList.add('d-none'), 4000);
|
|
}
|
|
|
|
function showHistoryStatus(message, type) {
|
|
const el = document.getElementById('historyModalStatus');
|
|
el.className = `alert alert-${type} mb-3`;
|
|
el.textContent = message;
|
|
el.classList.remove('d-none');
|
|
}
|
|
|
|
function setHistoryActionState(enabled) {
|
|
['historyApproveBtn', 'historyInvestigatingBtn', 'historyReadyBtn', 'historyCreateSagBtn']
|
|
.forEach(id => {
|
|
const btn = document.getElementById(id);
|
|
if (btn) btn.disabled = !enabled;
|
|
});
|
|
}
|
|
|
|
function renderInvoiceLines(lines) {
|
|
if (!lines || lines.length === 0) {
|
|
return '<div class="text-muted small">Ingen linjer fundet</div>';
|
|
}
|
|
|
|
return `
|
|
<div class="table-responsive mt-2">
|
|
<table class="table table-sm table-bordered mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Linje</th>
|
|
<th>Varenr</th>
|
|
<th>Beskrivelse</th>
|
|
<th>Antal</th>
|
|
<th>Pris</th>
|
|
<th>Beløb</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${lines.map(line => `
|
|
<tr>
|
|
<td>${formatNumber(line.line_number)}</td>
|
|
<td>${escapeHtml(line.product_number || '-')}</td>
|
|
<td class="text-truncate" style="max-width: 420px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(line.description || line.product_name || '-')}">${escapeHtml(line.description || line.product_name || '-')}</td>
|
|
<td>${formatNumber(line.quantity)}</td>
|
|
<td>${formatCurrency(line.unit_price)}</td>
|
|
<td>${formatCurrency(line.line_net_amount)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderInvoiceNotes(invoice) {
|
|
const parts = [invoice.heading, invoice.note_text].filter(Boolean);
|
|
if (parts.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
return `
|
|
<div class="border rounded bg-light p-2 mb-2 small">
|
|
${parts.map(part => `<div>${escapeHtml(part)}</div>`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderInvoiceCard(invoice, monthKey, index) {
|
|
const collapseId = `invoice-lines-${monthKey}-${index}`;
|
|
return `
|
|
<div class="border rounded p-2 mb-2 bg-white">
|
|
<div class="d-flex justify-content-between align-items-center gap-2">
|
|
<div class="text-truncate small" style="min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(`${invoice.invoice_number || '-'} (${invoice.invoice_date || '-'}) · ${invoice.heading || invoice.source_type || ''} · Netto: ${formatCurrency(invoice.net_amount)} · Moms: ${formatCurrency(invoice.vat_amount)} · Total: ${formatCurrency(invoice.total_amount)}`)}">
|
|
<strong>${escapeHtml(invoice.invoice_number || '-')}</strong>
|
|
<span class="text-muted">(${escapeHtml(invoice.invoice_date || '-')})</span>
|
|
<span class="text-muted">· ${escapeHtml(invoice.heading || invoice.source_type || '')}</span>
|
|
<span>· Netto: ${formatCurrency(invoice.net_amount)} · Moms: ${formatCurrency(invoice.vat_amount)} · Total: ${formatCurrency(invoice.total_amount)}</span>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="false">
|
|
Vis linjer
|
|
</button>
|
|
</div>
|
|
<div class="collapse" id="${collapseId}">
|
|
${renderInvoiceNotes(invoice)}
|
|
${renderInvoiceLines(invoice.lines)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function showInvoiceHistory(issueId) {
|
|
const body = document.getElementById('historyModalBody');
|
|
const meta = document.getElementById('historyModalMeta');
|
|
const status = document.getElementById('historyModalStatus');
|
|
currentHistoryIssueId = issueId;
|
|
setHistoryActionState(true);
|
|
status.classList.add('d-none');
|
|
meta.textContent = '';
|
|
body.innerHTML = '<tr><td colspan="6" class="text-muted text-center py-4">Indlæser historik...</td></tr>';
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('historyModal'));
|
|
modal.show();
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/invoice-error-finder/issues/${issueId}/invoice-history`);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Kunne ikke hente historik');
|
|
|
|
meta.textContent = `${data.customer_name || 'Ukendt kunde'} · ${data.product_name || data.product_number || '-'}`;
|
|
|
|
body.innerHTML = data.months.map(row => {
|
|
const stateBadge = row.line_count > 0
|
|
? '<span class="badge bg-success">Faktureret</span>'
|
|
: '<span class="badge bg-danger">Manglende</span>';
|
|
const monthBadge = row.is_reference_month
|
|
? ' <span class="badge bg-warning text-dark">Reference</span>'
|
|
: row.is_fallback_history
|
|
? ` <span class="badge bg-info text-dark">${escapeHtml(row.fallback_label || 'Seneste tidligere faktura')}</span>`
|
|
: '';
|
|
const invoiceList = row.invoices && row.invoices.length
|
|
? row.invoices.map((invoice, index) => renderInvoiceCard(invoice, row.month_start || 'month', index)).join('')
|
|
: '<span class="text-muted">Ingen faktura</span>';
|
|
|
|
return `
|
|
<tr class="${row.is_reference_month ? 'table-warning' : row.is_fallback_history ? 'table-info' : ''}">
|
|
<td>${formatMonth(row.month_start)}${monthBadge}</td>
|
|
<td>${stateBadge}</td>
|
|
<td>${formatNumber(row.line_count)}</td>
|
|
<td>${formatNumber(row.total_quantity)}</td>
|
|
<td>${formatCurrency(row.total_amount)}</td>
|
|
<td>${invoiceList}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
} catch (err) {
|
|
body.innerHTML = '<tr><td colspan="6" class="text-danger text-center py-4">Kunne ikke hente historik</td></tr>';
|
|
showHistoryStatus(`Fejl: ${err.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
document.getElementById('historyApproveBtn')?.addEventListener('click', function() {
|
|
if (!currentHistoryIssueId) return;
|
|
updateStatus(currentHistoryIssueId, 'approved_change', { target: 'history', successMessage: 'Fejlen er godkendt' });
|
|
});
|
|
|
|
document.getElementById('historyInvestigatingBtn')?.addEventListener('click', function() {
|
|
if (!currentHistoryIssueId) return;
|
|
updateStatus(currentHistoryIssueId, 'investigating', { target: 'history', successMessage: 'Fejlen er sat til undersøgelse' });
|
|
});
|
|
|
|
document.getElementById('historyReadyBtn')?.addEventListener('click', function() {
|
|
if (!currentHistoryIssueId) return;
|
|
updateStatus(currentHistoryIssueId, 'ready_to_invoice', { target: 'history', successMessage: 'Ordrekladde markeret som klar' });
|
|
});
|
|
|
|
document.getElementById('historyCreateSagBtn')?.addEventListener('click', function() {
|
|
if (!currentHistoryIssueId) return;
|
|
createSag(currentHistoryIssueId, { target: 'history' });
|
|
});
|
|
|
|
loadIssues();
|
|
</script>
|
|
{% endblock %}
|