bmc_hub/app/modules/invoice_error_finder/templates/dashboard.html
2026-07-10 16:22:57 +02:00

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 %}