257 lines
11 KiB
HTML
257 lines
11 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Faktura-fejl-finder - 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-finder</h1>
|
|
<p class="text-muted mb-0">Find abonnementer, varer og salgsordrer som burde være faktureret, men ikke er blevet det.</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-outline-primary" onclick="importEconomic()">
|
|
<i class="bi bi-cloud-download me-1"></i>Importér e-conomic
|
|
</button>
|
|
<button class="btn btn-outline-primary" onclick="importSimply()">
|
|
<i class="bi bi-cloud-download me-1"></i>Importér Simply
|
|
</button>
|
|
<button class="btn btn-primary" onclick="runAnalysis()">
|
|
<i class="bi bi-search me-1"></i>Kør analyse
|
|
</button>
|
|
<a href="/invoice-error-finder/issues" class="btn btn-outline-secondary">
|
|
<i class="bi bi-list-ul me-1"></i>Fejlliste
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="importStatus" class="alert d-none mb-4"></div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-12 col-sm-6 col-lg-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small mb-2">Manglende varelinjer</h6>
|
|
<h2 class="mb-0" id="missingLineCount">-</h2>
|
|
</div>
|
|
<div class="bg-danger bg-opacity-10 p-2 rounded">
|
|
<i class="bi bi-file-x text-danger fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<a href="/invoice-error-finder/issues?issue_type=missing_line" class="stretched-link"></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-sm-6 col-lg-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small mb-2">Åbne ordrer uden faktura</h6>
|
|
<h2 class="mb-0" id="openOrderCount">-</h2>
|
|
</div>
|
|
<div class="bg-warning bg-opacity-10 p-2 rounded">
|
|
<i class="bi bi-cart-x text-warning fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<a href="/invoice-error-finder/issues?issue_type=open_order_not_invoiced" class="stretched-link"></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-sm-6 col-lg-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small mb-2">Antalsfald</h6>
|
|
<h2 class="mb-0" id="quantityDropCount">-</h2>
|
|
</div>
|
|
<div class="bg-info bg-opacity-10 p-2 rounded">
|
|
<i class="bi bi-graph-down-arrow text-info fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<a href="/invoice-error-finder/issues?issue_type=quantity_drop" class="stretched-link"></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-sm-6 col-lg-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small mb-2">Prisændringer</h6>
|
|
<h2 class="mb-0" id="priceChangeCount">-</h2>
|
|
</div>
|
|
<div class="bg-primary bg-opacity-10 p-2 rounded">
|
|
<i class="bi bi-currency-exchange text-primary fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<a href="/invoice-error-finder/issues?issue_type=price_change" class="stretched-link"></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-12 col-md-4">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small mb-2">Klar til fakturering</h6>
|
|
<h2 class="mb-0" id="readyToInvoiceCount">-</h2>
|
|
</div>
|
|
<div class="bg-success bg-opacity-10 p-2 rounded">
|
|
<i class="bi bi-check-circle text-success fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<a href="/invoice-error-finder/issues?status=ready_to_invoice" class="stretched-link"></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small mb-2">Fejl uden ansvarlig</h6>
|
|
<h2 class="mb-0" id="noOwnerCount">-</h2>
|
|
</div>
|
|
<div class="bg-secondary bg-opacity-10 p-2 rounded">
|
|
<i class="bi bi-person-x text-secondary fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<a href="/invoice-error-finder/issues?assigned_user_id=null" class="stretched-link"></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small mb-2">Seneste importkørsler</h6>
|
|
<ul class="list-unstyled mb-0 small" id="lastImportRuns">
|
|
<li class="text-muted">Indlæser...</li>
|
|
</ul>
|
|
</div>
|
|
<div class="bg-light p-2 rounded">
|
|
<i class="bi bi-clock-history text-muted fs-4"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function loadDashboard() {
|
|
try {
|
|
const res = await fetch('/api/v1/invoice-error-finder/dashboard');
|
|
if (!res.ok) {
|
|
let detail = 'Kunne ikke hente dashboard';
|
|
try {
|
|
const payload = await res.json();
|
|
if (res.status === 403) {
|
|
detail = 'Du mangler adgang til Faktura-fejl-finder';
|
|
} else if (payload?.detail) {
|
|
detail = payload.detail;
|
|
}
|
|
} catch (error) {
|
|
if (res.status === 403) {
|
|
detail = 'Du mangler adgang til Faktura-fejl-finder';
|
|
}
|
|
}
|
|
throw new Error(detail);
|
|
}
|
|
const data = await res.json();
|
|
|
|
document.getElementById('missingLineCount').textContent = data.missing_line?.count ?? 0;
|
|
document.getElementById('openOrderCount').textContent = data.open_order_not_invoiced?.count ?? 0;
|
|
document.getElementById('quantityDropCount').textContent = data.quantity_drop?.count ?? 0;
|
|
document.getElementById('priceChangeCount').textContent = data.price_change?.count ?? 0;
|
|
document.getElementById('readyToInvoiceCount').textContent = data.ready_to_invoice?.count ?? 0;
|
|
document.getElementById('noOwnerCount').textContent = data.no_owner?.count ?? 0;
|
|
|
|
const runsList = document.getElementById('lastImportRuns');
|
|
if (data.last_import_runs && data.last_import_runs.length > 0) {
|
|
runsList.innerHTML = data.last_import_runs.map(run => {
|
|
const date = new Date(run.started_at).toLocaleString('da-DK');
|
|
const icon = run.status === 'success' ? '✅' : run.status === 'partial' ? '⚠️' : '❌';
|
|
return `<li>${icon} ${run.source_type}: ${run.records_imported} importeret (${date})</li>`;
|
|
}).join('');
|
|
} else {
|
|
runsList.innerHTML = '<li class="text-muted">Ingen importer endnu</li>';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
showStatus('Fejl ved indlæsning af dashboard: ' + err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function importEconomic() {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch('/api/v1/invoice-error-finder/import/economic', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Import fejlede');
|
|
showStatus(`e-conomic import færdig: ${data.records_imported} importeret, ${data.records_failed} fejlede.`, 'success');
|
|
await loadDashboard();
|
|
} catch (err) {
|
|
showStatus('e-conomic import fejlede: ' + err.message, 'danger');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function importSimply() {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch('/api/v1/invoice-error-finder/import/simply', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Import fejlede');
|
|
showStatus(`Simply import færdig: ${data.records_imported} importeret, ${data.records_failed} fejlede.`, 'success');
|
|
await loadDashboard();
|
|
} catch (err) {
|
|
showStatus('Simply import fejlede: ' + err.message, 'danger');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function runAnalysis() {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch('/api/v1/invoice-error-finder/analyze', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || 'Analyse fejlede');
|
|
const counts = Object.entries(data.counts || {})
|
|
.map(([k, v]) => `${k}: ${v}`)
|
|
.join(', ');
|
|
showStatus(`Analyse færdig for ${data.reference_month}. ${counts}`, 'success');
|
|
await loadDashboard();
|
|
} catch (err) {
|
|
showStatus('Analyse fejlede: ' + err.message, 'danger');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
function showStatus(message, type) {
|
|
const el = document.getElementById('importStatus');
|
|
el.className = `alert alert-${type} mb-4`;
|
|
el.textContent = message;
|
|
el.classList.remove('d-none');
|
|
}
|
|
|
|
function setLoading(loading) {
|
|
document.querySelectorAll('button').forEach(btn => btn.disabled = loading);
|
|
}
|
|
|
|
loadDashboard();
|
|
</script>
|
|
{% endblock %}
|