Files
boc/landvex-bokforing/dashboard-source.html
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

217 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LandveX AB — Bokföring (Live)</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg:#FAF9F6; --navy:#001E50; --amber:#C8763A; --card:#FFFFFF; --border:#E8E3DC;
--muted:#6B6B70; --green:#34C759; --red:#FF3B30;
--shadow:0 1px 4px rgba(0,30,80,.07),0 4px 16px rgba(0,30,80,.06); --radius:12px;
--font:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
}
html { font-family:var(--font); font-size:15px; background:var(--bg); color:var(--navy); }
a { color:var(--amber); text-decoration:none; }
#topbar { position:sticky; top:0; z-index:100; background:var(--navy); color:#fff;
display:flex; align-items:center; gap:16px; padding:0 28px; height:56px; box-shadow:0 2px 8px rgba(0,0,0,.18); }
.brand-logo { width:30px; height:30px; border-radius:50%; background:var(--amber); display:flex;
align-items:center; justify-content:center; font-size:15px; font-weight:800; color:#fff; }
.brand-name { font-size:.95rem; font-weight:700; }
#company { font-size:.85rem; color:rgba(255,255,255,.7); margin-left:8px; }
.topbar-right { margin-left:auto; display:flex; align-items:center; gap:12px; font-size:.82rem; color:rgba(255,255,255,.75); }
#refreshBtn { background:rgba(255,255,255,.12); border:1px solid rgba(255,255,255,.25); color:#fff;
border-radius:8px; padding:6px 12px; font-size:.82rem; cursor:pointer; }
#refreshBtn:hover { background:rgba(255,255,255,.2); }
.wrap { max-width:1180px; margin:0 auto; padding:24px 28px 80px; }
.tabs { display:flex; gap:6px; margin:18px 0 22px; flex-wrap:wrap; }
.tab { padding:8px 16px; border-radius:9px; background:var(--card); border:1px solid var(--border);
font-size:.88rem; font-weight:600; cursor:pointer; color:var(--muted); }
.tab.active { background:var(--navy); color:#fff; border-color:var(--navy); }
.cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:14px; margin-bottom:24px; }
.kpi { background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:18px 20px; box-shadow:var(--shadow); }
.kpi .label { font-size:.78rem; color:var(--muted); font-weight:600; text-transform:uppercase; letter-spacing:.03em; }
.kpi .val { font-size:1.55rem; font-weight:800; margin-top:6px; }
.kpi .sub { font-size:.78rem; color:var(--muted); margin-top:4px; }
.kpi.ok .val { color:var(--green); } .kpi.warn .val { color:var(--amber); } .kpi.bad .val { color:var(--red); }
.panel { background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); margin-bottom:22px; overflow:hidden; }
.panel h2 { font-size:1rem; padding:16px 20px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; }
.panel h2 .cnt { font-size:.8rem; color:var(--muted); font-weight:500; }
table { width:100%; border-collapse:collapse; font-size:.85rem; }
td { overflow-wrap:anywhere; }
th { text-align:left; padding:10px 14px; background:#F5F2ED; color:var(--muted); font-weight:600; font-size:.76rem; text-transform:uppercase; letter-spacing:.03em; position:sticky; top:0; z-index:5; box-shadow:0 1px 0 var(--border); }
td { padding:9px 14px; border-top:1px solid var(--border); vertical-align:top; line-height:1.45; }
td.num, th.num { text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap; }
tr:hover td { background:#FBFAF7; }
.tablewrap { max-height:640px; overflow:auto; }
.pos { color:var(--green); } .neg { color:var(--red); }
.badge { display:inline-block; padding:2px 8px; border-radius:6px; font-size:.72rem; font-weight:600; }
.b-bank { background:#E3F0FF; color:#0050A0; } .b-manual { background:#FFF0E0; color:#A05000; }
.vline { font-size:.8rem; color:var(--muted); }
.vline b { color:var(--navy); font-weight:600; }
.hidden { display:none; }
.loading { text-align:center; padding:60px; color:var(--muted); }
.err { background:#FFE8E6; color:#A02020; padding:14px 20px; border-radius:9px; margin:20px 0; font-size:.86rem; }
.src { font-size:.74rem; color:var(--muted); font-family:monospace; }
.foot { text-align:center; font-size:.76rem; color:var(--muted); margin-top:30px; }
</style>
</head>
<body>
<div id="topbar">
<div class="brand-logo">L</div>
<span class="brand-name">LandveX Bokföring</span>
<span id="company">org.nr 559141-7042 · brutet räkenskapsår majapr</span>
<div class="topbar-right">
<span id="lastsync"></span>
<button id="refreshBtn" onclick="loadAll()">↻ Uppdatera</button>
</div>
</div>
<div class="wrap">
<div id="errbox"></div>
<div class="cards" id="kpis"></div>
<div class="tabs">
<div class="tab active" data-tab="verifikat">Verifikat</div>
<div class="tab" data-tab="saldon">Kontosaldon</div>
<div class="tab" data-tab="kund">Kundreskontra</div>
<div class="tab" data-tab="bank">Bankavstämning</div>
</div>
<div id="tab-verifikat"></div>
<div id="tab-saldon" class="hidden"></div>
<div id="tab-kund" class="hidden"></div>
<div id="tab-bank" class="hidden"></div>
<div class="foot">Alla siffror hämtas live från aamos-ledger (tenant: landvex). Inga hårdkodade värden.</div>
</div>
<script>
const API = '/api/ledger';
const TENANT = 'landvex';
const H = { 'X-Tenant-ID': TENANT };
let STATE = { entries:[], tb2025:[], tb2026:[] };
const fmt = n => Number(n).toLocaleString('sv-SE',{minimumFractionDigits:2,maximumFractionDigits:2});
const ACCNAMES = {
'1221':'Inventarier/verktyg','1227':'Inventarier','1310':'Andelar','1510':'Kundfordringar',
'1610':'Fordran anställd Leon','1689':'Övr kortfr fordran','1930':'Nordea företagskonto',
'1932':'Revolut företagskonto','2013':'Egna uttag','2098':'IB Revolut','2099':'Ingående eget kapital',
'2611':'Utgående moms 25%','2650':'Momsredovisning','2710':'Personalskatt','2731':'Arb.giv.avgift',
'2890':'Skuld','2990':'Oklassificerat (skuld)','3000':'Försäljning','3010':'Försäljning tjänst',
'3020':'Försäljning','3900':'Övr intäkt','3960':'Valutakursvinst','4630':'Lego/UE',
'5010':'Lokalhyra','5612':'Drivmedel','5614':'Fordonskostnad','5800':'Resekostnader',
'6071':'Representation','6230':'IT/datakommunikation','6540':'IT-tjänster','6570':'Bankkostnader',
'6990':'Övriga kostnader','7010':'Löner'
};
const accName = a => ACCNAMES[a] || '';
async function loadAll() {
document.getElementById('errbox').innerHTML='';
try {
const [j, t26, t25] = await Promise.all([
fetch(`${API}/journal?limit=2000`,{headers:H}).then(r=>r.json()),
fetch(`${API}/trial-balance?fiscal_year=2026`,{headers:H}).then(r=>r.json()),
fetch(`${API}/trial-balance?fiscal_year=2025`,{headers:H}).then(r=>r.json()),
]);
STATE.entries = (j.entries||[]).sort((a,b)=> new Date(b.entry_date)-new Date(a.entry_date) || Number(b.entry_number)-Number(a.entry_number));
STATE.tb2025 = t25.accounts||[];
STATE.tb2026 = t26.accounts||[];
document.getElementById('lastsync').textContent = 'Synkad ' + new Date().toLocaleTimeString('sv-SE');
render();
} catch(e) {
document.getElementById('errbox').innerHTML = `<div class="err">Kunde inte hämta data: ${e.message}</div>`;
}
}
// Cumulative balance per account across both fiscal years
function cumBalances() {
const m = {};
for (const a of [...STATE.tb2025, ...STATE.tb2026]) {
if (!m[a.account_number]) m[a.account_number] = 0;
m[a.account_number] += Number(a.balance);
}
return m;
}
function render() {
const bal = cumBalances();
const nordea = bal['1930']||0, revolut = bal['1932']||0;
let totD=0, totK=0;
for (const e of STATE.entries) for (const l of (e.lines||[])) { totD+=Number(l.debit||0); totK+=Number(l.credit||0); }
const diff = Math.abs(totD-totK);
// KPIs
document.getElementById('kpis').innerHTML = `
<div class="kpi"><div class="label">Verifikat</div><div class="val">${STATE.entries.length}</div><div class="sub">bokförda poster</div></div>
<div class="kpi"><div class="label">Nordea 1930</div><div class="val">${fmt(nordea)}</div><div class="sub">SEK</div></div>
<div class="kpi"><div class="label">Revolut 1932</div><div class="val">${fmt(revolut)}</div><div class="sub">SEK</div></div>
<div class="kpi ${diff<0.01?'ok':'bad'}"><div class="label">Balans D=K</div><div class="val">${diff<0.01?'✓':fmt(diff)}</div><div class="sub">${fmt(totD)} / ${fmt(totK)}</div></div>`;
renderVerifikat(); renderSaldon(bal); renderKund(); renderBank(bal);
}
function renderVerifikat() {
let rows='';
for (const e of STATE.entries) {
const lines = (e.lines||[]).map(l=>{
const d=Number(l.debit||0), k=Number(l.credit||0);
return `<div class="vline"><b>${l.account_number}</b> ${accName(l.account_number)||l.account_name||''} ${d?'<span style="color:#34C759">D '+fmt(d)+'</span>':''}${k?'<span style="color:#FF3B30">K '+fmt(k)+'</span>':''}</div>`;
}).join('');
const dt = new Date(e.entry_date).toISOString().slice(0,10);
const src = e.source_id ? `<div class="src">src: ${e.source_id}</div>` : '';
rows += `<tr><td class="num">#${e.entry_number}</td><td>${dt}</td><td>${e.description||''}<span class="badge b-${e.source_type||'manual'}" style="margin-left:6px">${e.source_type||''}</span>${src}</td><td>${lines}</td></tr>`;
}
document.getElementById('tab-verifikat').innerHTML = `
<div class="panel"><h2>Verifikationer <span class="cnt">${STATE.entries.length} st · senaste först</span></h2>
<div class="tablewrap"><table><thead><tr><th class="num">Ver#</th><th>Datum</th><th>Beskrivning / källa</th><th>Konteringar</th></tr></thead>
<tbody>${rows}</tbody></table></div></div>`;
}
function renderSaldon(bal) {
const keys = Object.keys(bal).filter(k=>Math.abs(bal[k])>0.005).sort();
let assets='', other='';
for (const k of keys) {
const v = bal[k];
const row = `<tr><td class="num"><b>${k}</b></td><td>${accName(k)}</td><td class="num ${v>=0?'pos':'neg'}">${fmt(v)}</td></tr>`;
if (k[0]==='1') assets+=row; else other+=row;
}
document.getElementById('tab-saldon').innerHTML = `
<div class="panel"><h2>Tillgångar (klass 1)</h2><table><thead><tr><th class="num">Konto</th><th>Namn</th><th class="num">Saldo</th></tr></thead><tbody>${assets}</tbody></table></div>
<div class="panel"><h2>Skulder, EK, resultat (klass 28)</h2><table><thead><tr><th class="num">Konto</th><th>Namn</th><th class="num">Saldo</th></tr></thead><tbody>${other}</tbody></table></div>`;
}
function renderKund() {
// Kundfordringar/betalningar: leta fakturor (3000-serie credit) och inbetalningar (1510/1930/1932 debit kund)
const inv = STATE.entries.filter(e => /faktura|elles|trygg bil|karlaplan|abycon|gorkha/i.test(e.description||''));
let rows='';
for (const e of inv) {
const dt = new Date(e.entry_date).toISOString().slice(0,10);
const amt = (e.lines||[]).reduce((s,l)=>s+Number(l.debit||0),0);
rows += `<tr><td class="num">#${e.entry_number}</td><td>${dt}</td><td>${e.description||''}</td><td class="num">${fmt(amt)}</td></tr>`;
}
document.getElementById('tab-kund').innerHTML = `
<div class="panel"><h2>Kund- & motpartsposter <span class="cnt">${inv.length} st</span></h2>
<div class="tablewrap"><table><thead><tr><th class="num">Ver#</th><th>Datum</th><th>Motpart / beskrivning</th><th class="num">Belopp (D)</th></tr></thead>
<tbody>${rows}</tbody></table></div></div>`;
}
function renderBank(bal) {
const rows = [
['Nordea 1930', bal['1930']||0, 201441.93],
['Revolut 1932', bal['1932']||0, 176598.27],
].map(([n,book,stmt])=>{
const d = Math.abs(book-stmt);
return `<tr><td>${n}</td><td class="num">${fmt(book)}</td><td class="num">${fmt(stmt)}</td><td class="num ${d<0.01?'pos':'neg'}">${d<0.01?'✓ 0,00':fmt(book-stmt)}</td></tr>`;
}).join('');
document.getElementById('tab-bank').innerHTML = `
<div class="panel"><h2>Bankavstämning mot kontoutdrag</h2>
<table><thead><tr><th>Konto</th><th class="num">Bokfört (live)</th><th class="num">Kontoutdrag</th><th class="num">Diff</th></tr></thead>
<tbody>${rows}</tbody></table>
<div style="padding:14px 20px; font-size:.8rem; color:var(--muted)">Bokfört = live summa ur huvudboken (båda räkenskapsåren). Kontoutdrag = faktiskt slutsaldo enligt Nordea (2026-06-08) resp Revolut (2026-06-09).</div></div>`;
}
document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
t.classList.add('active');
['verifikat','saldon','kund','bank'].forEach(n=>document.getElementById('tab-'+n).classList.toggle('hidden', n!==t.dataset.tab));
});
loadAll();
</script>
</body>
</html>