Files
boc/design/finance-current.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

3012 lines
168 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 Finance</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#F7F6F3;--navy:#001E50;--amber:#C8763A;--card:#fff;--border:#E5E0D8;
--muted:#6B6B70;--green:#27AE60;--red:#E53E3E;--blue:#2563EB;
--navy-05:#001E500A;--navy-10:#001E5012;--amber-bg:#C8763A14;
--shadow:0 1px 3px rgba(0,30,80,.08),0 4px 12px rgba(0,30,80,.06);
--r:10px;--font:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
}
html{font-family:var(--font);font-size:14px;background:var(--bg);color:var(--navy)}
body{min-height:100vh}
button{font-family:var(--font);cursor:pointer}
input,select{font-family:var(--font)}
/* ── Topbar ── */
/* ── TOPBAR — tvårads layout ── */
#topbar{
position:sticky;top:0;z-index:100;background:var(--navy);color:#fff;
display:flex;flex-direction:column;
box-shadow:0 2px 10px rgba(0,0,0,.3);
}
#topbar-top{
display:flex;align-items:center;gap:10px;padding:0 20px;height:52px;
border-bottom:1px solid rgba(255,255,255,.07);
}
#topbar-tabs{
display:flex;align-items:stretch;background:rgba(0,0,0,.18);
padding:0 8px;
}
.brand{display:flex;align-items:center;gap:10px;flex-shrink:0}
.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}
.brand-name{font-size:.95rem;font-weight:700;white-space:nowrap}
.brand-co{font-size:.8rem;color:rgba(255,255,255,.5);margin-left:2px;white-space:nowrap}
/* Tabbar — full bredd, understreck-stil, ingen scroll */
#tab-nav{display:flex;align-items:stretch;flex:1;gap:0}
.tab-btn{
background:transparent;border:none;border-bottom:3px solid transparent;
color:rgba(255,255,255,.55);
padding:10px 18px;font-size:.83rem;font-weight:600;cursor:pointer;
white-space:nowrap;transition:color .15s,border-color .15s;
display:flex;align-items:center;gap:5px;
}
.tab-btn:hover{color:rgba(255,255,255,.9);border-bottom-color:rgba(255,255,255,.3)}
.tab-btn.active{color:#fff;border-bottom:3px solid var(--amber);font-weight:700}
/* period */
.period-bar{display:flex;align-items:center;gap:5px;flex-shrink:0}
.pmode-group{display:flex;background:rgba(255,255,255,.1);border-radius:7px;padding:2px;gap:1px}
.pmode-btn{background:transparent;border:none;color:rgba(255,255,255,.6);
padding:4px 10px;border-radius:5px;font-size:.76rem;font-weight:600;cursor:pointer;white-space:nowrap}
.pmode-btn.active{background:var(--amber);color:#fff}
.psel{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);
color:#fff;border-radius:7px;padding:5px 9px;font-size:.8rem;cursor:pointer;outline:none}
.psel option{background:#001E50}
.pcustom{display:none;align-items:center;gap:4px}
.pcustom-in{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);
color:#fff;border-radius:7px;padding:4px 7px;font-size:.78rem;outline:none;width:108px}
.pcustom-in::-webkit-calendar-picker-indicator{filter:invert(1) brightness(.7)}
/* status + actions */
.topbar-right{display:flex;align-items:center;gap:8px;margin-left:4px;flex-shrink:0}
#data-status{display:flex;align-items:center;gap:5px;font-size:.74rem;color:rgba(255,255,255,.6);min-width:44px;justify-content:flex-end}
.sdot{width:7px;height:7px;border-radius:50%;background:var(--muted)}
.sdot.live{background:var(--green);box-shadow:0 0 0 2px rgba(39,174,96,.3)}
.sdot.demo{background:var(--amber)}
.sdot.err{background:var(--red)}
.btn-top{background:var(--amber);border:none;color:#fff;border-radius:7px;
padding:7px 16px;font-size:.82rem;font-weight:600;cursor:pointer;white-space:nowrap}
.btn-top:hover{background:#b0612e}
/* ── Main ── */
#main{max-width:1320px;margin:0 auto;padding:20px 18px 0}
.tab-sec{display:none}.tab-sec.active{display:block}
/* ── Cards ── */
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);
box-shadow:var(--shadow);margin-bottom:14px;overflow:hidden}
.card-hdr{display:flex;align-items:center;justify-content:space-between;
padding:12px 16px;border-bottom:1px solid var(--border);background:var(--navy-05)}
.card-title{font-size:.88rem;font-weight:700}
.card-body{padding:14px 16px}
.badge{font-size:.7rem;font-weight:700;padding:2px 8px;border-radius:5px;background:var(--navy-10)}
/* ── KPI row ── */
.kpi-row{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:14px}
.kpi[onclick]:hover{transform:translateY(-2px);box-shadow:0 4px 16px rgba(0,0,0,.12);transition:transform .15s,box-shadow .15s}
.kpi[onclick]{transition:transform .15s,box-shadow .15s}
.kpi{background:var(--card);border:1px solid var(--border);border-radius:var(--r);
padding:16px 18px;box-shadow:var(--shadow);position:relative;overflow:hidden}
.kpi::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:3px 0 0 3px}
.kpi.green::before{background:var(--green)}.kpi.red::before{background:var(--red)}
.kpi.amber::before{background:var(--amber)}.kpi.navy::before{background:var(--navy)}
.kpi.blue::before{background:var(--blue)}
.kpi-icon{font-size:1.1rem;margin-bottom:6px}
.kpi-val{font-size:1.35rem;font-weight:800;font-variant-numeric:tabular-nums;margin-bottom:3px}
.kpi-val.green{color:var(--green)}.kpi-val.red{color:var(--red)}
.kpi-val.amber{color:var(--amber)}.kpi-val.navy{color:var(--navy)}
.kpi-lbl{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
/* ── Two-col layout ── */
.two-col{display:grid;grid-template-columns:1fr 340px;gap:14px}
/* ── P&L ── */
.pl-sec-title{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;
color:var(--muted);margin-bottom:7px;padding-bottom:4px;border-bottom:1px solid var(--border)}
.pl-row{display:flex;justify-content:space-between;align-items:center;
padding:5px 4px;border-radius:5px;font-size:.84rem}
.pl-row:hover{background:var(--navy-05)}
.pl-acct{color:var(--muted)}
.pl-code{font-size:.68rem;color:var(--muted);background:var(--navy-10);border-radius:3px;padding:1px 4px;margin-left:5px}
.pl-amt{font-weight:600;font-variant-numeric:tabular-nums}
.pl-total{display:flex;justify-content:space-between;padding:7px 8px;
background:var(--navy-05);border-radius:6px;margin:6px 0}
.pl-total-lbl{font-size:.84rem;font-weight:700}
.pl-total-amt{font-size:.9rem;font-weight:800;font-variant-numeric:tabular-nums}
.pl-result{display:flex;justify-content:space-between;align-items:center;
padding:10px 14px;border-radius:8px;margin-top:10px}
.pl-result.pos{background:rgba(39,174,96,.08);border:1px solid rgba(39,174,96,.2)}
.pl-result.neg{background:rgba(229,62,62,.06);border:1px solid rgba(229,62,62,.2)}
.pl-result-lbl{font-size:.85rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em}
.pl-result-amt{font-size:1.2rem;font-weight:800;font-variant-numeric:tabular-nums}
/* ── Right col ── */
.rc-actions{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
.act-btn{display:flex;align-items:center;gap:5px;padding:7px 12px;border-radius:7px;
font-size:.8rem;font-weight:600;border:1.5px solid var(--border);background:transparent;
color:var(--navy);cursor:pointer;transition:all .15s;white-space:nowrap}
.act-btn:hover{border-color:var(--amber);background:var(--amber-bg)}
.act-btn.primary{background:var(--amber);border-color:var(--amber);color:#fff}
.act-btn.primary:hover{background:#b0612e}
/* ── Journal widget ── */
.jw-row{display:grid;grid-template-columns:52px 78px 1fr auto;
gap:8px;align-items:center;padding:6px 4px;border-bottom:1px solid var(--border);cursor:pointer}
.jw-row:last-child{border-bottom:none}
.jw-row:hover{background:var(--navy-05);border-radius:6px;margin:0 -4px;padding:6px 8px}
.jw-nr{font-size:.7rem;color:var(--muted);background:var(--navy-10);border-radius:4px;
padding:2px 5px;text-align:center;font-variant-numeric:tabular-nums}
.jw-date{font-size:.75rem;color:var(--muted);font-variant-numeric:tabular-nums}
.jw-desc{font-size:.82rem;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0}
.jw-amt{font-size:.82rem;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
.tag{font-size:.66rem;font-weight:700;padding:1px 4px;border-radius:3px;
background:var(--amber-bg);color:var(--amber);margin-left:3px;vertical-align:middle}
.tag.bank{background:rgba(37,99,235,.1);color:var(--blue)}
/* ── Period badge ── */
.pbadge{display:inline-flex;align-items:center;gap:4px;font-size:.75rem;font-weight:600;
padding:3px 9px;border-radius:20px}
.pbadge.open{background:rgba(39,174,96,.1);color:#1a8a45}
.pbadge.review{background:rgba(200,118,58,.12);color:var(--amber)}
.pbadge.closed{background:var(--navy-10);color:var(--muted)}
/* ── TRANSAKTIONER-FLIK ── */
.tx-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap}
.tx-search{flex:1;min-width:200px;border:1.5px solid var(--border);border-radius:8px;
padding:8px 12px;font-size:.85rem;outline:none;background:#fff}
.tx-search:focus{border-color:var(--amber)}
.tx-filter-sel{border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;
font-size:.82rem;outline:none;background:#fff;cursor:pointer}
.tx-filter-sel:focus{border-color:var(--amber)}
.tx-table{width:100%;border-collapse:collapse}
.tx-table th{text-align:left;font-size:.68rem;font-weight:700;text-transform:uppercase;
letter-spacing:.06em;color:var(--muted);padding:7px 10px;border-bottom:2px solid var(--border);
white-space:nowrap;background:var(--navy-05)}
.tx-table td{padding:8px 10px;border-bottom:1px solid var(--border);font-size:.83rem;vertical-align:middle}
.tx-table tr:hover td{background:var(--navy-05);cursor:pointer}
.tx-nr{font-size:.72rem;color:var(--muted);background:var(--navy-10);border-radius:4px;
padding:2px 5px;font-variant-numeric:tabular-nums;white-space:nowrap}
.tx-amt{font-weight:600;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}
.tx-empty{text-align:center;padding:40px;color:var(--muted)}
.tx-pagination{display:flex;align-items:center;justify-content:space-between;
padding:10px 0;margin-top:4px;font-size:.82rem;color:var(--muted)}
.pg-btn{border:1.5px solid var(--border);background:#fff;border-radius:7px;
padding:5px 12px;font-size:.8rem;cursor:pointer;font-weight:600}
.pg-btn:hover:not(:disabled){border-color:var(--amber);color:var(--amber)}
.pg-btn:disabled{opacity:.4;cursor:default}
.src-bank{color:var(--blue);font-size:.7rem;font-weight:700}
.src-manual{color:var(--muted);font-size:.7rem;font-weight:700}
/* ── TX MOBILKORT ── */
.tx-cards{display:none}
.tx-card{
display:flex;align-items:center;gap:12px;
padding:13px 14px;border-bottom:1px solid var(--border);
cursor:pointer;transition:background .12s;
-webkit-tap-highlight-color:transparent;
}
.tx-card:last-child{border-bottom:none}
.tx-card:active{background:var(--navy-05)}
.tx-card-left{
flex-shrink:0;width:42px;height:42px;border-radius:10px;
background:var(--navy-05);display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:1px;
}
.tx-card-nr{font-size:.62rem;font-weight:700;color:var(--muted);font-variant-numeric:tabular-nums}
.tx-card-src-dot{width:6px;height:6px;border-radius:50%;background:var(--muted)}
.tx-card-src-dot.bank{background:var(--blue)}
.tx-card-body{flex:1;min-width:0}
.tx-card-desc{
font-size:.88rem;font-weight:600;color:var(--navy);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
margin-bottom:3px;
}
.tx-card-meta{display:flex;align-items:center;gap:8px;font-size:.74rem;color:var(--muted)}
.tx-card-accts{font-size:.72rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:160px}
.tx-card-right{flex-shrink:0;text-align:right}
.tx-card-amt{font-size:.9rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--navy);white-space:nowrap}
.tx-card-status{font-size:.68rem;margin-top:2px}
.tx-card-empty{padding:40px 16px;text-align:center;color:var(--muted)}
/* Mobilsök — stor, täcker hela bredden */
@media(max-width:640px){
.tx-toolbar{gap:6px}
.tx-search{font-size:.92rem;padding:10px 14px}
.tx-filter-row{display:flex;gap:6px}
.tx-filter-sel{flex:1;font-size:.82rem;padding:9px 8px}
/* Växla tabell <-> kort */
.tx-table-wrap{display:none}
.tx-cards{display:block}
/* Paginering: större knappar */
.pg-btn{padding:10px 20px;font-size:.85rem}
.tx-pagination{gap:8px}
}
/* ── FAKTUROR-FLIK ── */
.inv-table{width:100%;border-collapse:collapse}
.inv-table th{text-align:left;font-size:.68rem;font-weight:700;text-transform:uppercase;
letter-spacing:.06em;color:var(--muted);padding:7px 10px;border-bottom:2px solid var(--border);
background:var(--navy-05)}
.inv-table td{padding:8px 10px;border-bottom:1px solid var(--border);font-size:.83rem}
.inv-table tr:hover td{background:var(--navy-05);cursor:pointer}
.inv-status{display:inline-flex;align-items:center;gap:3px;font-size:.7rem;font-weight:700;
padding:2px 7px;border-radius:10px}
.inv-status.paid{background:rgba(39,174,96,.1);color:#1a8a45}
.inv-status.sent{background:rgba(200,118,58,.12);color:var(--amber)}
.inv-status.overdue{background:rgba(229,62,62,.1);color:var(--red)}
/* ── MOMS ── */
.vat-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:14px}
.vat-card{background:var(--navy-05);border:1px solid var(--border);border-radius:8px;padding:12px 14px}
.vat-ruta{font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);margin-bottom:3px}
.vat-lbl{font-size:.8rem;color:var(--muted);margin-bottom:5px}
.vat-amt{font-size:1.1rem;font-weight:800;font-variant-numeric:tabular-nums}
.vat-note{font-size:.68rem;color:var(--muted);margin-top:3px}
.vat-total-row{display:flex;justify-content:space-between;align-items:center;
padding:14px 16px;border-radius:8px;background:var(--navy);color:#fff;margin-bottom:10px}
.vat-total-lbl{font-size:.88rem;font-weight:700}
.vat-total-amt{font-size:1.3rem;font-weight:800;font-variant-numeric:tabular-nums;color:#ff9f7a}
.vat-alert{padding:10px 14px;border-radius:8px;font-size:.82rem;font-weight:600;margin-bottom:12px}
.vat-alert.warn{background:rgba(229,62,62,.08);border:1px solid rgba(229,62,62,.25);color:var(--red)}
/* ── PERIODSTÄNGNING ── */
.wf-steps{display:flex;align-items:center;gap:3px;margin-bottom:14px;flex-wrap:wrap}
.wf-step{padding:4px 10px;border-radius:5px;font-size:.75rem;font-weight:600;
background:var(--navy-10);color:var(--muted)}
.wf-step.active{background:var(--amber);color:#fff}
.wf-step.done{background:rgba(39,174,96,.15);color:var(--green)}
.wf-arrow{color:var(--muted);font-size:.85rem}
.chk-list{list-style:none}
.chk-item{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--border);font-size:.85rem}
.chk-item:last-child{border-bottom:none}
.chk-icon{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;
justify-content:center;font-size:.78rem;flex-shrink:0}
.chk-icon.ok{background:rgba(39,174,96,.15);color:var(--green)}
.chk-icon.warn{background:rgba(229,62,62,.1);color:var(--red)}
.chk-icon.pending{background:var(--navy-10);color:var(--muted)}
.chk-val{font-size:.78rem;font-weight:600;color:var(--muted);margin-left:auto;font-variant-numeric:tabular-nums}
/* ── NY VERIFIKATION ── */
.entry-toggle{width:100%;display:flex;align-items:center;justify-content:space-between;
padding:13px 16px;background:transparent;border:none;cursor:pointer;text-align:left}
.entry-toggle:hover{background:var(--navy-05)}
.entry-body{display:none;padding:16px;border-top:1px solid var(--border)}
.entry-body.open{display:block}
.form-row{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:12px}
.form-grp{display:flex;flex-direction:column;gap:4px}
.form-lbl{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
.form-in{border:1.5px solid var(--border);border-radius:7px;padding:8px 10px;
font-size:.87rem;outline:none;background:#fff;transition:border-color .15s}
.form-in:focus{border-color:var(--amber)}
.lines-hdr{display:grid;grid-template-columns:1fr 110px 110px 32px;gap:6px;
padding:0 0 5px;font-size:.68rem;font-weight:700;text-transform:uppercase;
letter-spacing:.05em;color:var(--muted)}
.e-line{display:grid;grid-template-columns:1fr 110px 110px 32px;gap:6px;margin-bottom:7px;align-items:start}
.acc-wrap{position:relative}
.acc-in{border:1.5px solid var(--border);border-radius:7px;padding:7px 9px;
font-size:.84rem;width:100%;outline:none;background:#fff;transition:border-color .15s}
.acc-in:focus{border-color:var(--amber)}
.acc-drop{position:absolute;top:100%;left:0;right:0;z-index:50;background:#fff;
border:1px solid var(--border);border-radius:7px;box-shadow:var(--shadow);
max-height:190px;overflow-y:auto;display:none}
.acc-drop.open{display:block}
.acc-opt{padding:7px 9px;font-size:.83rem;cursor:pointer;display:flex;align-items:center;gap:7px}
.acc-opt:hover,.acc-opt.focused{background:var(--navy-05)}
.acc-code{font-size:.72rem;color:var(--muted);background:var(--navy-10);border-radius:3px;
padding:1px 4px;flex-shrink:0;font-variant-numeric:tabular-nums}
.num-in{border:1.5px solid var(--border);border-radius:7px;padding:7px 9px;
font-size:.84rem;text-align:right;width:100%;outline:none;background:#fff;
font-variant-numeric:tabular-nums;transition:border-color .15s}
.num-in:focus{border-color:var(--amber)}
.rm-btn{background:transparent;border:1.5px solid var(--border);border-radius:6px;
color:var(--muted);font-size:.8rem;width:30px;height:34px;display:flex;
align-items:center;justify-content:center;flex-shrink:0}
.rm-btn:hover{border-color:var(--red);color:var(--red)}
#add-line-btn{background:transparent;border:1.5px dashed var(--border);border-radius:7px;
color:var(--muted);font-size:.82rem;padding:7px;width:100%;margin-bottom:12px}
#add-line-btn:hover{border-color:var(--amber);color:var(--amber)}
.bal-ind{display:flex;align-items:center;justify-content:space-between;
padding:9px 12px;border-radius:7px;background:var(--navy-05);border:1.5px solid var(--border);margin-bottom:12px}
.bal-ind.ok{background:rgba(39,174,96,.07);border-color:rgba(39,174,96,.3)}
.bal-ind.bad{background:rgba(229,62,62,.05);border-color:rgba(229,62,62,.2)}
.form-actions{display:flex;align-items:center;gap:8px}
.btn-primary{background:var(--navy);color:#fff;border:none;border-radius:7px;
padding:9px 18px;font-size:.87rem;font-weight:600}
.btn-primary:hover{background:#002878}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.btn-secondary{background:transparent;border:1.5px solid var(--border);color:var(--muted);
border-radius:7px;padding:9px 18px;font-size:.87rem;font-weight:600}
.btn-secondary:hover{border-color:var(--navy);color:var(--navy)}
.btn-outline{border:1.5px solid var(--border);background:transparent;
padding:7px 13px;font-size:.8rem;font-weight:600;color:var(--navy);border-radius:7px}
.btn-outline:hover{border-color:var(--navy);background:var(--navy-05)}
.feedback{font-size:.82rem;color:var(--muted);display:none}
.feedback.ok{display:block;color:var(--green)}
.feedback.err{display:block;color:var(--red)}
/* ── Modal ── */
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:200;
display:flex;align-items:center;justify-content:center;padding:16px}
.modal-bg.hidden{display:none}
.modal-box{background:#fff;border-radius:var(--r);box-shadow:0 8px 40px rgba(0,30,80,.2);
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;overflow:hidden}
.modal-hdr{display:flex;align-items:center;justify-content:space-between;
padding:16px 20px;border-bottom:1px solid var(--border);background:var(--navy-05);flex-shrink:0}
.modal-title{font-size:.95rem;font-weight:700}
.modal-close{background:none;border:none;font-size:1.1rem;color:var(--muted);cursor:pointer;line-height:1;padding:2px 6px}
.modal-close:hover{color:var(--navy)}
.modal-body{padding:20px;overflow-y:auto;flex:1}
.modal-footer{padding:12px 20px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end;flex-shrink:0}
/* entry detail table */
.det-table{width:100%;border-collapse:collapse;font-size:.83rem;margin-top:10px}
.det-table th{text-align:left;font-size:.68rem;font-weight:700;text-transform:uppercase;
letter-spacing:.06em;color:var(--muted);padding:5px 8px;border-bottom:2px solid var(--border)}
.det-table td{padding:7px 8px;border-bottom:1px solid var(--border)}
.det-total{background:var(--navy-05);font-weight:700}
.det-meta{margin-top:12px;padding:10px 12px;background:var(--navy-05);border-radius:7px;font-size:.78rem;color:var(--muted)}
.det-meta strong{color:var(--navy)}
/* ── Footer ── */
.fc-bar-col{display:flex;flex-direction:column;align-items:center;flex:1;min-width:0}
.fc-bar{border-radius:4px 4px 0 0;width:100%;min-height:3px}
.fc-bar.pos{background:var(--green)}.fc-bar.neg{background:var(--red)}.fc-bar.warn{background:var(--amber)}
.fc-bv{font-size:.62rem;color:var(--muted);margin-top:2px;text-align:center;font-variant-numeric:tabular-nums}
.act-row{display:flex;gap:8px;align-items:flex-start;padding:7px 0;border-bottom:1px solid var(--border);font-size:.81rem}
.act-row:last-child{border-bottom:none}
.act-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:4px}
.act-dot.r{background:var(--red)}.act-dot.a{background:var(--amber)}.act-dot.g{background:var(--green)}
.rec{padding:8px 10px;border-radius:7px;margin-bottom:6px;font-size:.8rem;line-height:1.5}
.rec.crit{background:rgba(229,62,62,.06);border-left:3px solid var(--red)}
.rec.warn{background:rgba(200,118,58,.07);border-left:3px solid var(--amber)}
.rec.info{background:rgba(37,99,235,.05);border-left:3px solid var(--blue)}
.cbar{display:flex;align-items:center;gap:8px;margin-bottom:7px}
.cbar-lbl{width:170px;flex-shrink:0;font-size:.78rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cbar-track{flex:1;height:8px;background:var(--navy-10);border-radius:4px;overflow:hidden}
.cbar-fill{height:100%;border-radius:4px}
.cbar-fill.high{background:var(--red)}.cbar-fill.med{background:var(--amber)}.cbar-fill.low{background:var(--green)}
.cbar-amt{width:90px;text-align:right;font-size:.78rem;font-weight:600;font-variant-numeric:tabular-nums;flex-shrink:0}
/* kvitton */
.drop-zone{border:2px dashed var(--border);border-radius:10px;padding:28px 20px;
text-align:center;cursor:pointer;transition:all .2s;background:var(--bg)}
.drop-zone:hover,.drop-zone.drag{border-color:var(--amber);background:var(--amber-bg)}
.drop-zone-icon{font-size:2rem;margin-bottom:8px}
.drop-zone-text{font-size:.85rem;color:var(--muted)}
.drop-zone-sub{font-size:.75rem;color:var(--muted);margin-top:4px}
.receipt-list{margin-top:14px}
.receipt-item{display:flex;align-items:center;gap:10px;padding:8px 10px;
background:var(--navy-05);border-radius:7px;margin-bottom:6px;font-size:.82rem}
.receipt-item-icon{font-size:1.1rem;flex-shrink:0}
.receipt-item-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.receipt-item-size{color:var(--muted);font-size:.72rem;white-space:nowrap}
.receipt-item-link{color:var(--amber);text-decoration:none;font-size:.78rem;white-space:nowrap}
.receipt-item-link:hover{text-decoration:underline}
.upload-progress{height:4px;background:var(--navy-10);border-radius:2px;margin-top:8px;overflow:hidden}
.upload-progress-fill{height:100%;background:var(--amber);border-radius:2px;transition:width .3s}
#footer{max-width:1320px;margin:16px auto 0;padding:12px 18px;border-top:1px solid var(--border);
display:flex;align-items:center;justify-content:space-between;font-size:.72rem;color:var(--muted)}
/* ── Hamburgermeny & mobilnavigation ── */
#ham-btn{
display:none;background:none;border:none;color:#fff;cursor:pointer;
padding:8px;border-radius:8px;flex-shrink:0;
transition:background .15s;
}
#ham-btn:hover{background:rgba(255,255,255,.12)}
#ham-btn svg{display:block}
/* Drawer */
#mob-drawer{
display:none;position:fixed;inset:0;z-index:2000;
}
#mob-drawer-backdrop{
position:absolute;inset:0;background:rgba(0,0,30,.55);backdrop-filter:blur(2px);
opacity:0;transition:opacity .22s ease;
}
#mob-drawer-panel{
position:absolute;top:0;left:0;bottom:0;width:82%;max-width:320px;
background:var(--navy);color:#fff;display:flex;flex-direction:column;
transform:translateX(-100%);transition:transform .25s cubic-bezier(.4,0,.2,1);
box-shadow:4px 0 24px rgba(0,0,0,.4);
}
#mob-drawer.open{display:block}
#mob-drawer.open #mob-drawer-backdrop{opacity:1}
#mob-drawer.open #mob-drawer-panel{transform:translateX(0)}
.drawer-header{
display:flex;align-items:center;justify-content:space-between;
padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);
flex-shrink:0;
}
.drawer-brand{display:flex;align-items:center;gap:10px}
.drawer-logo{width:34px;height:34px;border-radius:50%;background:var(--amber);
display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:800}
.drawer-title{font-size:.95rem;font-weight:700}
.drawer-sub{font-size:.75rem;color:rgba(255,255,255,.5)}
.drawer-close{background:none;border:none;color:rgba(255,255,255,.7);cursor:pointer;
padding:6px;border-radius:8px;font-size:1.3rem;line-height:1}
.drawer-close:hover{background:rgba(255,255,255,.12)}
.drawer-nav{
flex:1;overflow-y:auto;padding:8px 0;
}
.drawer-nav-item{
display:flex;align-items:center;gap:14px;padding:15px 20px;
font-size:.97rem;font-weight:600;color:rgba(255,255,255,.75);
cursor:pointer;border:none;background:none;width:100%;text-align:left;
transition:background .12s,color .12s;
border-left:3px solid transparent;
}
.drawer-nav-item:hover{background:rgba(255,255,255,.08);color:#fff}
.drawer-nav-item.active{color:#fff;background:rgba(200,118,58,.18);border-left-color:var(--amber)}
.drawer-nav-item .nav-icon{font-size:1.2rem;width:26px;text-align:center;flex-shrink:0}
.drawer-nav-item .nav-badge{
margin-left:auto;background:var(--red);color:#fff;font-size:.65rem;font-weight:700;
padding:2px 7px;border-radius:10px;
}
.drawer-footer{
padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);
display:flex;flex-direction:column;gap:8px;
}
.drawer-period-label{font-size:.72rem;color:rgba(255,255,255,.5);font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px}
.drawer-psel{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);
color:#fff;border-radius:8px;padding:9px 12px;font-size:.85rem;width:100%;outline:none}
.drawer-psel option{background:#001E50}
.drawer-role-btn{
display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.07);
border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.75);
border-radius:8px;padding:10px 14px;font-size:.85rem;font-weight:600;
cursor:pointer;width:100%;text-align:left;
}
.drawer-role-btn:hover{background:rgba(255,255,255,.13);color:#fff}
/* FAB — snabbknapp */
#fab-receipt{
display:none;
position:fixed;right:18px;bottom:24px;z-index:1500;
width:60px;height:60px;border-radius:50%;
background:var(--amber);color:#fff;border:none;
box-shadow:0 4px 20px rgba(200,118,58,.6),0 2px 6px rgba(0,0,0,.2);
font-size:1.7rem;cursor:pointer;
align-items:center;justify-content:center;
transition:transform .15s,box-shadow .15s;
}
#fab-receipt:hover{transform:scale(1.08);box-shadow:0 6px 28px rgba(200,118,58,.75)}
#fab-receipt:active{transform:scale(.93)}
/* Aktiv flik-indikator i topbar (mobil) */
#mob-current-tab{
display:none;
align-items:center;gap:6px;
font-size:.87rem;font-weight:700;color:#fff;
background:rgba(255,255,255,.1);border-radius:8px;
padding:5px 12px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:160px;
}
/* ── Responsive ── */
/* ── Tablet ── */
@media(max-width:1000px){
.kpi-row{grid-template-columns:repeat(3,1fr)}
.two-col{grid-template-columns:1fr}
.vat-grid{grid-template-columns:1fr 1fr}
#topbar-top{padding:0 12px;height:46px;gap:8px}
.brand-name{display:none}
.period-bar{order:3;width:100%}
.topbar-right{order:2}
}
/* ── Mobil ── */
@media(max-width:640px){
/* Göm desktop-tabbar, visa hamburgare */
#topbar-tabs{display:none}
.period-bar{display:none}
#ham-btn{display:flex;align-items:center;justify-content:center}
#fab-receipt{display:flex}
#mob-current-tab{display:flex}
/* Dölj desktop-only topbar-knappar */
.btn-top{display:none}
/* Topbar kompakt */
#topbar-top{padding:0 10px;height:52px;gap:8px}
.brand-name{display:block;font-size:.88rem}
.brand-co{display:none}
#main{padding:12px 10px 90px}
.kpi-row{grid-template-columns:repeat(2,1fr);gap:8px}
.kpi{padding:12px 14px}
.kpi-val{font-size:1.1rem}
.two-col{grid-template-columns:1fr}
.form-row{grid-template-columns:1fr}
.vat-grid{grid-template-columns:1fr}
/* Tabell göms på mobil, kort visas istället via .tx-table-wrap / .tx-cards */
.inv-table th:nth-child(4),
.inv-table td:nth-child(4),
.inv-table th:nth-child(5),
.inv-table td:nth-child(5){display:none}
.modal-box{max-height:95vh;margin:0;border-radius:var(--r) var(--r) 0 0;align-self:flex-end}
.modal-bg{align-items:flex-end;padding:0}
.tx-toolbar{flex-direction:column;align-items:stretch}
.tx-search{min-width:unset}
.rc-actions{gap:5px}
.act-btn{padding:6px 10px;font-size:.77rem}
#footer{flex-direction:column;gap:4px;text-align:center}
}
/* ═══════════════════════════════════════════════════════════
TOOLTIP SYSTEM
═══════════════════════════════════════════════════════════ */
[data-tip]{position:relative;cursor:help}
[data-tip]::after{
content:attr(data-tip);
position:absolute;
bottom:calc(100% + 8px);
left:50%;
transform:translateX(-50%);
background:#1a2740;
color:#f0f4ff;
font-size:.75rem;
font-weight:400;
line-height:1.45;
padding:7px 11px;
border-radius:7px;
white-space:pre-wrap;
max-width:240px;
min-width:120px;
text-align:left;
box-shadow:0 4px 16px rgba(0,0,0,.35);
pointer-events:none;
opacity:0;
transition:opacity .15s ease, transform .15s ease;
transform:translateX(-50%) translateY(4px);
z-index:9999;
}
[data-tip]::before{
content:'';
position:absolute;
bottom:calc(100% + 3px);
left:50%;
transform:translateX(-50%);
border:5px solid transparent;
border-top-color:#1a2740;
pointer-events:none;
opacity:0;
transition:opacity .15s ease;
z-index:9999;
}
[data-tip]:hover::after,[data-tip]:hover::before{opacity:1;transform:translateX(-50%) translateY(0)}
/* Tooltips som öppnas nedåt (för top-nav) */
[data-tip-down]{position:relative;cursor:help}
[data-tip-down]::after{
content:attr(data-tip-down);
position:absolute;
top:calc(100% + 8px);
bottom:auto;
left:50%;
transform:translateX(-50%) translateY(-4px);
background:#1a2740;
color:#f0f4ff;
font-size:.75rem;
font-weight:400;
line-height:1.45;
padding:7px 11px;
border-radius:7px;
white-space:pre-wrap;
max-width:240px;
min-width:120px;
text-align:left;
box-shadow:0 4px 16px rgba(0,0,0,.35);
pointer-events:none;
opacity:0;
transition:opacity .15s ease, transform .15s ease;
z-index:9999;
}
[data-tip-down]::before{
content:'';
position:absolute;
top:calc(100% + 3px);
bottom:auto;
left:50%;
transform:translateX(-50%);
border:5px solid transparent;
border-top-color:transparent;
border-bottom-color:#1a2740;
pointer-events:none;
opacity:0;
transition:opacity .15s ease;
z-index:9999;
}
[data-tip-down]:hover::after,[data-tip-down]:hover::before{opacity:1;transform:translateX(-50%) translateY(0)}
/* ═══════════════════════════════════════════════════════════
ONBOARDING OVERLAY
═══════════════════════════════════════════════════════════ */
#ob-backdrop{
position:fixed;inset:0;
background:rgba(0,0,0,.55);
z-index:10000;
display:flex;align-items:center;justify-content:center;
}
#ob-box{
background:#fff;
border-radius:16px;
padding:32px 36px;
max-width:460px;
width:90%;
box-shadow:0 20px 60px rgba(0,0,0,.35);
position:relative;
animation:ob-in .25s ease;
}
@keyframes ob-in{from{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}
#ob-box.dark{background:#0f1b2d;color:#e8eeff}
#ob-step-num{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#6b7fa3;margin-bottom:8px}
#ob-icon{font-size:2.4rem;margin-bottom:10px}
#ob-title{font-size:1.25rem;font-weight:800;margin-bottom:8px;color:#0f1b2d}
#ob-text{font-size:.92rem;line-height:1.6;color:#3a4a6b;margin-bottom:20px}
#ob-box.dark #ob-title{color:#e8eeff}
#ob-box.dark #ob-text{color:#a0b0d0}
#ob-actions{display:flex;align-items:center;justify-content:space-between;gap:12px}
#ob-dots{display:flex;gap:6px}
.ob-dot{width:7px;height:7px;border-radius:50%;background:#d0d8e8;transition:background .2s}
.ob-dot.active{background:#1a56e0}
#ob-skip{font-size:.78rem;color:#6b7fa3;cursor:pointer;background:none;border:none;padding:4px 8px}
#ob-skip:hover{color:#1a56e0}
#ob-next{background:#1a56e0;color:#fff;border:none;border-radius:9px;padding:9px 22px;
font-size:.88rem;font-weight:700;cursor:pointer;transition:background .15s}
#ob-next:hover{background:#1445b8}
#ob-highlight{
position:fixed;
border-radius:10px;
box-shadow:0 0 0 4px #1a56e0, 0 0 0 9999px rgba(0,0,0,.4);
transition:all .3s ease;
pointer-events:none;
z-index:9999;
}
#ob-close{position:absolute;top:12px;right:14px;background:none;border:none;
font-size:1.1rem;cursor:pointer;color:#6b7fa3;padding:4px}
</style>
</head>
<body>
<!-- HAMBURGERMENY DRAWER -->
<div id="mob-drawer" role="dialog" aria-modal="true" aria-label="Navigation">
<div id="mob-drawer-backdrop" onclick="closeMobDrawer()"></div>
<div id="mob-drawer-panel">
<div class="drawer-header">
<div class="drawer-brand">
<div class="drawer-logo"></div>
<div>
<div class="drawer-title">Ouroboros Finance</div>
<div class="drawer-sub" id="drawer-co-name">LandveX AB</div>
</div>
</div>
<button class="drawer-close" onclick="closeMobDrawer()" aria-label="Stäng"></button>
</div>
<nav class="drawer-nav" id="mob-drawer-nav">
<button class="drawer-nav-item active" data-tab="overview" onclick="mobNavTo('overview')">
<span class="nav-icon">📊</span> Översikt
</button>
<button class="drawer-nav-item" data-tab="transactions" onclick="mobNavTo('transactions')">
<span class="nav-icon">📋</span> Transaktioner
</button>
<button class="drawer-nav-item" data-tab="invoices" onclick="mobNavTo('invoices')">
<span class="nav-icon">🧾</span> Fakturor
</button>
<button class="drawer-nav-item" data-tab="vat" onclick="mobNavTo('vat')">
<span class="nav-icon">🏦</span> Skatt & Moms
</button>
<button class="drawer-nav-item" data-tab="payroll" onclick="mobNavTo('payroll')">
<span class="nav-icon">👥</span> Löner
</button>
<button class="drawer-nav-item" data-tab="period" onclick="mobNavTo('period')">
<span class="nav-icon">📅</span> Period
</button>
<button class="drawer-nav-item" data-tab="tasks" onclick="mobNavTo('tasks')">
<span class="nav-icon"></span> Tasks
<span class="nav-badge" id="drawer-tasks-badge" style="display:none">!</span>
</button>
<button class="drawer-nav-item" data-tab="forecast" onclick="mobNavTo('forecast')">
<span class="nav-icon">📈</span> Prognos
</button>
</nav>
<div class="drawer-footer">
<div>
<div class="drawer-period-label">Period</div>
<select class="drawer-psel" id="drawer-period-sel" onchange="syncDrawerPeriod(this.value)"></select>
</div>
<button class="drawer-role-btn" onclick="closeMobDrawer();obShowRolePicker()">
<span>👤</span> <span id="drawer-role-name">Byt roll / guide</span>
</button>
</div>
</div>
</div>
<!-- FAB — snabbknapp för kvitto/utlägg -->
<button id="fab-receipt" onclick="fabAddReceipt()" aria-label="Lägg till kvitto / utlägg">
📷
</button>
<!-- TOPBAR -->
<div id="topbar">
<!-- Rad 1: brand + period + status -->
<div id="topbar-top">
<button id="ham-btn" onclick="openMobDrawer()" aria-label="Meny" aria-expanded="false">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
<line x1="2" y1="5.5" x2="20" y2="5.5"/>
<line x1="2" y1="11" x2="20" y2="11"/>
<line x1="2" y1="16.5" x2="20" y2="16.5"/>
</svg>
</button>
<div class="brand">
<div class="brand-logo"></div>
<span class="brand-name">Ouroboros Finance</span>
<span class="brand-co" id="co-name">LandveX AB</span>
</div>
<div id="mob-current-tab">📊 Översikt</div>
<div style="flex:1"></div>
<div class="period-bar">
<div class="pmode-group">
<button class="pmode-btn active" id="pmode-month" onclick="setPeriodMode('month')">Mån</button>
<button class="pmode-btn" id="pmode-quarter" onclick="setPeriodMode('quarter')">Kv</button>
<button class="pmode-btn" id="pmode-custom" onclick="setPeriodMode('custom')"></button>
</div>
<div id="pw-month"><select class="psel" id="sel-month" data-tip="Välj vilket månad du
vill se bokföringen för."></select></div>
<div id="pw-quarter" style="display:none"><select class="psel" id="sel-quarter"></select></div>
<div class="pcustom" id="pw-custom">
<input type="month" class="pcustom-in" id="p-from">
<span style="color:rgba(255,255,255,.4);font-size:.8rem"></span>
<input type="month" class="pcustom-in" id="p-to">
<button class="pmode-btn active" style="background:var(--green);margin-left:2px" onclick="applyCustom()">OK</button>
</div>
</div>
<div class="topbar-right">
<div id="data-status"><span class="sdot" id="sdot"></span><span id="stext"></span></div>
<button class="btn-top" onclick="openNewEntry()" data-tip="Skapa ett nytt verifikat
med debet- och kreditrader"> Verif.</button>
<button class="btn-top" onclick="obShowRolePicker()" data-tip="Starta om
introduktionsguiden" style="background:none;border:1px solid rgba(255,255,255,.3);color:rgba(255,255,255,.8)">?</button>
</div>
</div>
</div>
<!-- Rad 2: tabbar full bredd -->
<div id="topbar-tabs">
<div id="tab-nav">
<button class="tab-btn active" onclick="switchTab('overview')" data-tip-down="Sammanfattning av kassa,
intäkter, kostnader och resultat">📊 Översikt</button>
<button class="tab-btn" onclick="switchTab('transactions')" data-tip-down="Alla bokförda verifikat.
Klicka för att se detaljer.">📋 Trans.</button>
<button class="tab-btn" onclick="switchTab('invoices')" data-tip-down="Skapa och hantera fakturor.
Betald/obetald status.">🧾 Fakturor</button>
<button class="tab-btn" onclick="switchTab('vat')" data-tip-down="Moms, skatter och SKV-skulder.">🏦 Skatt</button>
<button class="tab-btn" onclick="switchTab('payroll')" id="payroll-tab-btn" data-tip-down="Löner och arbetsgivaravgifter.">👥 Löner</button>
<button class="tab-btn" onclick="switchTab('period')" data-tip-down="Öppna/stäng bokföringsperioder.">📅 Period</button>
<button class="tab-btn" onclick="switchTab('tasks')" id="tasks-tab-btn" data-tip-down="Åtgärder som kräver
din uppmärksamhet.">✅ Tasks</button>
<button class="tab-btn" onclick="switchTab('forecast')" data-tip-down="Likviditetsprognos
baserad på kända in/utflöden.">📈 Prognos</button>
</div>
</div>
</div>
<!-- MAIN -->
<div id="main">
<!-- ═══ ÖVERSIKT ═══ -->
<div id="tab-overview" class="tab-sec active">
<div class="kpi-row">
<div class="kpi" id="kpi-cash" onclick="showKassaModal()" style="cursor:pointer" title="Klicka för detaljer">
<div class="kpi-icon" data-tip="Total likvida medel på
alla bank- och kassakonton.">🏦</div>
<div class="kpi-val" id="kpi-cash-v"></div>
<div class="kpi-lbl">Kassa (19301936) </div>
</div>
<div class="kpi" id="kpi-rev" onclick="showIntakterModal()" style="cursor:pointer" title="Klicka för detaljer">
<div class="kpi-icon" data-tip="Totala intäkter hittills
denna period (konto 3xxx).">📈</div>
<div class="kpi-val amber" id="kpi-rev-v"></div>
<div class="kpi-lbl">Intäkter </div>
</div>
<div class="kpi" id="kpi-exp" onclick="showKostnaderModal()" style="cursor:pointer" title="Klicka för detaljer">
<div class="kpi-icon" data-tip="Totala kostnader hittills
denna period (konto 47xxx).">📉</div>
<div class="kpi-val navy" id="kpi-exp-v"></div>
<div class="kpi-lbl">Kostnader </div>
</div>
<div class="kpi" id="kpi-net" onclick="showResultatModal()" style="cursor:pointer" title="Klicka för detaljer">
<div class="kpi-icon" data-tip="Resultat = Intäkter minus Kostnader.
Positivt = vinst, negativt = förlust.">💰</div>
<div class="kpi-val" id="kpi-net-v"></div>
<div class="kpi-lbl">Resultat </div>
</div>
<div class="kpi red" onclick="showSkuldModal()" style="cursor:pointer" title="Klicka för detaljer">
<div class="kpi-icon" data-tip="Utgående moms som ska
betalas till Skatteverket.">🧾</div>
<div class="kpi-val red" id="kpi-vat-v"></div>
<div class="kpi-lbl">Moms att betala </div>
</div>
</div>
<div class="two-col">
<!-- P&L -->
<div class="card">
<div class="card-hdr">
<span class="card-title">📊 Resultaträkning</span>
<span class="badge" id="pl-badge"></span>
</div>
<div class="card-body" id="pl-body"><div style="text-align:center;padding:30px;color:var(--muted)">Laddar…</div></div>
</div>
<!-- Höger -->
<div>
<!-- Snabbåtgärder -->
<div style="margin-bottom:12px">
<div class="rc-actions">
<button class="act-btn primary" onclick="openNewEntry()" data-tip="Bokför en transaktion manuellt.
Ange konton och belopp."> Ny verifikation</button>
<button class="act-btn" onclick="switchTab('invoices');openNewInvoice()" data-tip="Skapa en ny utgående
faktura till kund.">🧾 Ny faktura</button>
<button class="act-btn" onclick="showBS()" data-tip="Visar balansräkning:
tillgångar, skulder och eget kapital.">📊 Balansräkning</button>
<button class="act-btn" onclick="doSIE()" data-tip="Exportera bokföringen i SIE4-format
till din revisor.">📤 SIE4</button>
</div>
</div>
<!-- Senaste verifikat -->
<div class="card" style="margin-bottom:12px">
<div class="card-hdr">
<span class="card-title">🗂 Senaste verifikat</span>
<button onclick="switchTab('transactions')" style="background:none;border:none;color:var(--amber);font-size:.78rem;font-weight:600;cursor:pointer">Visa alla →</button>
</div>
<div class="card-body" style="padding:8px 12px" id="jw-body">Laddar…</div>
</div>
<!-- Tasks / Checkpoints -->
<div class="card" id="tasks-badge-card" style="margin-bottom:12px;display:none">
<div class="card-hdr">
<span class="card-title" id="tasks-badge-title">⚠️ Kräver åtgärd</span>
<button onclick="switchTab('tasks')" style="background:none;border:none;color:var(--amber);font-size:.78rem;font-weight:600;cursor:pointer">Visa alla →</button>
</div>
<div class="card-body" id="tasks-badge-body" style="padding:8px 12px;font-size:.8rem"></div>
</div>
<!-- Period -->
<div class="card">
<div class="card-hdr"><span class="card-title">📅 Periodstatus</span></div>
<div class="card-body">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<div class="pbadge open" id="ov-period-badge">● Öppen</div>
<div style="font-size:.75rem;color:var(--muted);margin-top:4px" id="ov-period-detail">Period öppen</div>
</div>
<button class="btn-outline" onclick="switchTab('period')">Hantera →</button>
</div>
</div>
</div>
</div>
</div>
<!-- Ny verifikation (expanderbar) -->
<div class="card" id="new-entry-card">
<button class="entry-toggle" onclick="toggleEntry()">
<span style="font-size:.88rem;font-weight:700;display:flex;align-items:center;gap:6px"><span style="color:var(--amber)"></span> Ny verifikation</span>
<span id="entry-chev" style="font-size:.75rem;color:var(--muted);transition:transform .2s"></span>
</button>
<div id="entry-body" class="entry-body">
<div class="form-row">
<div class="form-grp"><label class="form-lbl">Datum</label><input type="date" class="form-in" id="e-date"></div>
<div class="form-grp"><label class="form-lbl">Beskrivning</label><input type="text" class="form-in" id="e-desc" placeholder="t.ex. Faktura #101 Trygg Bil"></div>
<div class="form-grp"><label class="form-lbl">Referens</label><input type="text" class="form-in" id="e-ref" placeholder="t.ex. FAK-2026-001"></div>
</div>
<div class="lines-hdr"><span>Konto</span><span style="text-align:right">Debet kr</span><span style="text-align:right">Kredit kr</span><span></span></div>
<div id="e-lines"></div>
<button id="add-line-btn" onclick="addLine()"> Lägg till rad</button>
<div class="bal-ind" id="bal-ind">
<div style="display:flex;gap:16px;font-size:.8rem">
<span>Debet: <strong id="bal-d">0 kr</strong></span>
<span>Kredit: <strong id="bal-k">0 kr</strong></span>
</div>
<span id="bal-txt" style="font-size:.8rem;color:var(--muted)">Ange rader</span>
</div>
<div class="form-actions">
<button class="btn-primary" id="save-btn" onclick="saveEntry()">💾 Spara</button>
<button class="btn-secondary" onclick="resetEntry()">Rensa</button>
<span class="feedback" id="e-fb"></span>
</div>
</div>
</div>
</div><!-- /översikt -->
<!-- TASKS TAB -->
<div id="tab-tasks" class="tab-sec">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<h2 style="margin:0;font-size:1rem;font-weight:600">✅ Åtgärdslista</h2>
<button class="act-btn primary" onclick="loadTasks()">🔄 Uppdatera</button>
</div>
<div id="tasks-list">Laddar…</div>
</div>
<!-- ═══ TRANSAKTIONER ═══ -->
<div id="tab-transactions" class="tab-sec">
<div class="card">
<div class="card-hdr">
<span class="card-title">📋 Alla transaktioner</span>
<span class="badge" id="tx-count-badge"></span>
</div>
<div class="card-body">
<div class="tx-toolbar">
<input type="text" class="tx-search" id="tx-search" placeholder="Sök beskrivning, referens, konto…" oninput="txSearchDebounce()">
<div class="tx-filter-row">
<select class="tx-filter-sel" id="tx-src" onchange="txLoad(1)">
<option value="">Alla källor</option>
<option value="bank">Bankimport</option>
<option value="manual">Manuella</option>
</select>
<select class="tx-filter-sel" id="tx-status-f" onchange="txLoad(1)">
<option value="posted">Bokförda</option>
<option value="">Alla status</option>
<option value="draft">Utkast</option>
<option value="void">Makulerade</option>
</select>
</div>
</div>
<!-- Desktop: tabell -->
<div class="tx-table-wrap">
<table class="tx-table">
<thead><tr>
<th>Nr</th><th>Datum</th><th>Beskrivning</th><th>Källa</th><th>Konton</th><th style="text-align:right">Belopp</th><th>Status</th>
</tr></thead>
<tbody id="tx-tbody"><tr><td colspan="7" class="tx-empty">Laddar…</td></tr></tbody>
</table>
</div>
<!-- Mobil: kort -->
<div class="tx-cards" id="tx-cards">Laddar…</div>
<!-- Paginering (delad) -->
<div class="tx-pagination">
<button class="pg-btn" id="pg-prev" onclick="txPage(-1)" disabled>← Föregående</button>
<span id="pg-info"></span>
<button class="pg-btn" id="pg-next" onclick="txPage(1)" disabled>Nästa →</button>
</div>
</div>
</div>
</div>
<!-- ═══ FAKTUROR ═══ -->
<div id="tab-invoices" class="tab-sec">
<div class="card">
<div class="card-hdr">
<span class="card-title">🧾 Fakturor</span>
<button class="act-btn primary" onclick="openNewInvoice()"> Ny faktura</button>
</div>
<div class="card-body">
<div id="inv-summary" style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:14px"></div>
<div id="inv-wrap">
<div id="inv-empty" style="text-align:center;padding:30px;color:var(--muted)">Laddar fakturor…</div>
<table class="inv-table" id="inv-table" style="display:none">
<thead><tr>
<th>Verifikat</th><th>Kund</th><th>Datum</th><th>Beskrivning</th>
<th style="text-align:right">Exkl moms</th><th style="text-align:right">Moms</th>
<th style="text-align:right">Totalt</th><th>Status</th>
</tr></thead>
<tbody id="inv-tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ═══ MOMS ═══ -->
<div id="tab-vat" class="tab-sec">
<!-- SKV-skulder — alltid högst upp -->
<div class="card" style="margin-bottom:12px">
<div class="card-hdr" style="background:rgba(255,59,48,.06)">
<span class="card-title" style="color:#FF3B30">🏛 Skatteskulder till Skatteverket</span>
<span id="skv-total-badge" class="badge" style="background:#FF3B30;color:#fff"></span>
</div>
<div id="skv-skulder-body">Laddar…</div>
</div>
<!-- Momsdeklarationsunderlag -->
<div class="card" style="margin-bottom:12px">
<div class="card-hdr">
<span class="card-title">🧮 Momsdeklarationsunderlag</span>
<div style="display:flex;gap:8px;align-items:center">
<select class="psel" id="vat-q-sel" onchange="loadVAT()" style="font-size:.78rem;padding:4px 8px">
<option value="Q1">Q1 JanMar 2026</option>
<option value="Q2" selected>Q2 AprJun 2026</option>
<option value="Q3">Q3 JulSep 2026</option>
<option value="Q4">Q4 OktDec 2026</option>
</select>
<button class="act-btn" onclick="exportVAT()">📄 Exportera</button>
</div>
</div>
<div class="card-body" id="vat-body">Laddar…</div>
</div>
<!-- Skattekalender -->
<div class="card">
<div class="card-hdr"><span class="card-title">📋 Skattekalender</span></div>
<div class="card-body" id="skv-calendar-body" style="padding:10px 16px;font-size:.82rem"></div>
</div>
</div></div>
<!-- ═══ LÖNER ═══ -->
<div id="tab-payroll" class="tab-sec">
<div class="kpi-row" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
<div class="kpi navy"><div class="kpi-icon">💼</div><div class="kpi-val navy" id="pr-lon-v"></div><div class="kpi-lbl">Bruttolöner YTD</div></div>
<div class="kpi amber"><div class="kpi-icon">🏛</div><div class="kpi-val amber" id="pr-aga-v"></div><div class="kpi-lbl">Arbetsgivaravgift YTD</div></div>
<div class="kpi red" onclick="showSkuldModal()" style="cursor:pointer"><div class="kpi-icon">⚠️</div><div class="kpi-val red" id="pr-skuld-v"></div><div class="kpi-lbl">Att betala SKV </div></div>
<div class="kpi"><div class="kpi-icon">👤</div><div class="kpi-val" id="pr-antal-v"></div><div class="kpi-lbl">Anställda / förskott</div></div>
</div>
<div class="card" style="margin-bottom:12px">
<div class="card-hdr">
<span class="card-title">👤 Löner per person</span>
<button class="act-btn primary" onclick="switchTab('transactions');openNewEntry()"> Bokför lön</button>
</div>
<div id="pr-persons-body" style="padding:0">Laddar…</div>
</div>
<div class="card" style="margin-bottom:12px">
<div class="card-hdr"><span class="card-title">📅 Löner per period</span></div>
<div id="pr-periods-body" style="padding:0">Laddar…</div>
</div>
<div class="card">
<div class="card-hdr" style="background:rgba(255,149,0,.08)">
<span class="card-title">🏛 Arbetsgivaravgifter &amp; personalskatt</span>
<span id="pr-skv-badge" class="badge"></span>
</div>
<div id="pr-skv-body" style="padding:10px 16px;font-size:.83rem">Laddar…</div>
</div>
</div>
<!-- ═══ PERIOD ═══ -->
<div id="tab-period" class="tab-sec">
<div class="card">
<div class="card-hdr">
<span class="card-title">📅 Periodstängning — <span id="pc-label"></span></span>
<div class="pbadge open" id="pc-badge">● Öppen</div>
</div>
<div class="card-body">
<div class="wf-steps" id="wf-steps">
<span class="wf-step active" id="wf-open">Öppen</span><span class="wf-arrow"></span>
<span class="wf-step" id="wf-review">Granskning</span><span class="wf-arrow"></span>
<span class="wf-step" id="wf-approved">Godkänd</span><span class="wf-arrow"></span>
<span class="wf-step" id="wf-closed">Stängd</span>
</div>
<ul class="chk-list" id="chk-list">
<li class="chk-item"><span class="chk-icon pending" id="ci-bal"></span><span>Balansräkning balanserar (debet = kredit)</span><span class="chk-val" id="cv-bal"></span></li>
<li class="chk-item"><span class="chk-icon pending" id="ci-obs"></span><span>Observationskonto 1689 = 0</span><span class="chk-val" id="cv-obs"></span></li>
<li class="chk-item"><span class="chk-icon pending" id="ci-vat"></span><span>Moms bokförd (konto 2611 ≠ 0)</span><span class="chk-val" id="cv-vat"></span></li>
<li class="chk-item"><span class="chk-icon pending" id="ci-dft"></span><span>Inga utkast-verifikat</span><span class="chk-val" id="cv-dft"></span></li>
</ul>
<div id="pc-actions" style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap"></div>
<div id="pc-fb" style="margin-top:8px;font-size:.82rem"></div>
<div id="pc-snapshot" style="display:none;margin-top:18px">
<div style="font-size:.85rem;font-weight:700;margin-bottom:8px">📸 Trial Balance</div>
<div id="pc-snap-content"></div>
</div>
</div>
</div>
</div>
<!-- ═══ PROGNOS ═══ -->
<div id="tab-forecast" class="tab-sec">
<div id="fc-alert" style="display:none;margin-bottom:14px;padding:12px 16px;border-radius:10px;font-size:.85rem;font-weight:600"></div>
<div class="kpi-row" style="grid-template-columns:repeat(4,1fr)">
<div class="kpi navy" onclick="showKassaModal()" style="cursor:pointer"><div class="kpi-icon">💰</div><div class="kpi-val navy" id="fc-kassa-v"></div><div class="kpi-lbl">Kassa idag </div></div>
<div class="kpi red" onclick="showSkuldModal()" style="cursor:pointer"><div class="kpi-icon">🧾</div><div class="kpi-val red" id="fc-skulder-v"></div><div class="kpi-lbl">Skulder förfallna </div></div>
<div class="kpi" id="fc-netto-card" onclick="showKassaModal()" style="cursor:pointer"><div class="kpi-icon">🏦</div><div class="kpi-val" id="fc-netto-v"></div><div class="kpi-lbl">Kassa efter skulder </div></div>
<div class="kpi amber"><div class="kpi-icon">🔥</div><div class="kpi-val amber" id="fc-burn-v"></div><div class="kpi-lbl">Burn rate /mån</div></div>
</div>
<div class="two-col" style="grid-template-columns:1fr 320px">
<div class="card">
<div class="card-hdr"><span class="card-title">📊 Likviditetsprognos — 6 månader</span><span class="badge" id="fc-runway"></span></div>
<div class="card-body">
<div id="fc-barchart" style="display:flex;align-items:flex-end;gap:6px;height:150px;margin-bottom:4px"></div>
<div id="fc-barlabels" style="display:flex;gap:6px;font-size:.68rem;color:var(--muted);margin-bottom:14px"></div>
<table style="width:100%;border-collapse:collapse;font-size:.82rem">
<thead><tr style="border-bottom:2px solid var(--border)">
<th style="padding:5px 8px;text-align:left;font-size:.69rem;color:var(--muted);text-transform:uppercase;font-weight:700">Månad</th>
<th style="padding:5px 8px;text-align:right;font-size:.69rem;color:var(--muted);text-transform:uppercase;font-weight:700">Inbetalt</th>
<th style="padding:5px 8px;text-align:right;font-size:.69rem;color:var(--muted);text-transform:uppercase;font-weight:700">Utbetalt</th>
<th style="padding:5px 8px;text-align:right;font-size:.69rem;color:var(--muted);text-transform:uppercase;font-weight:700">Netto</th>
<th style="padding:5px 8px;text-align:right;font-size:.69rem;color:var(--muted);text-transform:uppercase;font-weight:700">Kassa</th>
</tr></thead>
<tbody id="fc-table-body"></tbody>
</table>
<div style="margin-top:10px;padding:8px 10px;background:var(--navy-05);border-radius:7px;font-size:.72rem;color:var(--muted)">
Prognosen baseras på bekräftat Trygg Bil-kontrakt (222 916 kr/mån) + historiska genomsnittskostnader. Inkluderar inga nya affärer.
</div>
</div>
</div>
<div>
<div class="card" style="margin-bottom:12px">
<div class="card-hdr" style="background:rgba(229,62,62,.06)"><span class="card-title" style="color:var(--red)">🚨 Kritiska åtgärder</span></div>
<div class="card-body" style="padding:10px 14px" id="fc-actions"></div>
</div>
<div class="card" style="margin-bottom:12px">
<div class="card-hdr"><span class="card-title">🎯 Budgetrekommendationer</span></div>
<div class="card-body" style="padding:10px 14px" id="fc-recs"></div>
</div>
<div class="card">
<div class="card-hdr"><span class="card-title">💡 Intäktspotential</span></div>
<div class="card-body" style="padding:10px 14px" id="fc-revenue"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-hdr"><span class="card-title">📉 Kostnadsnedbrytning (snitt/mån)</span><span class="badge" id="fc-total-burn"></span></div>
<div class="card-body" id="fc-costbars" style="padding:12px 16px"></div>
</div>
</div>
</div><!-- /main -->
<!-- Kvitto-modal -->
<div id="modal-receipt" class="modal-bg hidden">
<div class="modal-box" style="max-width:520px">
<div class="modal-hdr">
<span class="modal-title" id="rcpt-title">📎 Kvitton</span>
<button class="modal-close" onclick="closeModal('modal-receipt')"></button>
</div>
<div class="modal-body">
<div id="rcpt-entry-info" style="margin-bottom:14px;padding:8px 12px;background:var(--navy-05);border-radius:7px;font-size:.8rem;color:var(--muted)"></div>
<!-- Befintliga kvitton -->
<div id="rcpt-existing"></div>
<!-- Upload-zon -->
<div class="drop-zone" id="rcpt-dropzone" onclick="document.getElementById('rcpt-file-input').click()">
<div class="drop-zone-icon">📁</div>
<div class="drop-zone-text">Klicka eller dra filer hit</div>
<div class="drop-zone-sub">JPG, PNG, PDF, HEIC — max 10 MB per fil</div>
</div>
<input type="file" id="rcpt-file-input" multiple accept="image/*,.pdf,.heic" style="display:none" onchange="handleReceiptFiles(this.files)">
<!-- Mobil: direkt kamera-knapp -->
<label id="rcpt-camera-btn" style="cursor:pointer;display:inline-flex;align-items:center;gap:8px;
background:var(--amber);color:#fff;border-radius:8px;
padding:9px 16px;font-size:.82rem;font-weight:600;margin-top:10px;width:100%;justify-content:center;box-sizing:border-box">
<input type="file" accept="image/*" capture="environment"
style="display:none" onchange="handleReceiptFiles(this.files)">
📷 Ta foto med kameran
</label>
<div id="rcpt-upload-list" style="margin-top:10px"></div>
<div id="rcpt-fb" style="margin-top:8px;font-size:.82rem"></div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeModal('modal-receipt')">Stäng</button>
</div>
</div>
</div>
<!-- Footer -->
<div id="footer">
<span id="ft-brand">Ouroboros Finance · LandveX AB · 2026</span>
<div style="display:flex;align-items:center;gap:10px">
<span id="ft-period"></span>
<span id="ft-count"></span>
</div>
</div>
<!-- Faktura-modal -->
<div id="modal-inv" class="modal-bg hidden">
<div class="modal-box">
<div class="modal-hdr"><span class="modal-title">🧾 Ny faktura</span><button class="modal-close" onclick="closeModal('modal-inv')"></button></div>
<div class="modal-body">
<div class="form-row" style="grid-template-columns:1fr 1fr">
<div class="form-grp"><label class="form-lbl">Kund</label><input type="text" class="form-in" id="inv-cust" placeholder="Kundnamn"></div>
<div class="form-grp"><label class="form-lbl">Fakturadatum</label><input type="date" class="form-in" id="inv-date"></div>
</div>
<div class="form-row" style="grid-template-columns:1fr 1fr">
<div class="form-grp"><label class="form-lbl">Förfallodatum</label><input type="date" class="form-in" id="inv-due"></div>
<div class="form-grp"><label class="form-lbl">Belopp exkl. moms (kr)</label><input type="number" class="form-in" id="inv-amt" min="0" step="0.01" oninput="previewInv()"></div>
</div>
<div class="form-row" style="grid-template-columns:1fr 1fr">
<div class="form-grp"><label class="form-lbl">Momssats</label>
<select class="form-in" id="inv-vat" onchange="previewInv()">
<option value="25">25%</option><option value="12">12%</option>
<option value="6">6%</option><option value="0">0%</option>
</select></div>
<div class="form-grp"><label class="form-lbl">Referens</label><input type="text" class="form-in" id="inv-ref" placeholder="ORD-2026-001"></div>
</div>
<div class="form-grp" style="margin-bottom:10px"><label class="form-lbl">Beskrivning</label><input type="text" class="form-in" id="inv-desc"></div>
<div id="inv-preview" style="padding:10px;background:var(--navy-05);border-radius:7px;font-size:.82rem;display:none"></div>
<div id="inv-fb" class="feedback" style="margin-top:8px"></div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeModal('modal-inv')">Avbryt</button>
<button class="btn-primary" id="inv-save" onclick="saveInvoice()">💾 Spara & bokför</button>
</div>
</div>
</div>
<!-- Info/detail modal -->
<div id="modal-info" class="modal-bg hidden">
<div class="modal-box">
<div class="modal-hdr"><span class="modal-title" id="mi-title"></span><button class="modal-close" onclick="closeModal('modal-info')"></button></div>
<div class="modal-body" id="mi-body"></div>
<div class="modal-footer"><button class="btn-secondary" onclick="closeModal('modal-info')">Stäng</button></div>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════════════════
// CONFIG
// ═══════════════════════════════════════════════════════════════════════
const FISCAL = 2026;
const TOKEN = 'wavult…2026';
const TENANT = 'landvex';
const MN = ['Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'];
const S = {
period: null, periodMode: 'month', token: TOKEN,
tb: [], journal: [], accounts: [], periods: [], invoices: [],
txPage: 1, txTotal: 0, txSearch: '', txSrc: '', txStatus: 'posted',
};
// ═══════════════════════════════════════════════════════════════════════
// UTILS
// ═══════════════════════════════════════════════════════════════════════
const fmt = v => { const a=Math.abs(v); return (v<0?'':'')+Math.round(a).toLocaleString('sv-SE')+' kr'; };
const esc = s => String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const today = () => new Date().toISOString().slice(0,10);
const now = () => new Date();
const acname= a => a.account_name || a.name || '';
const fbal = a => parseFloat(a.balance||0);
const fdeb = a => parseFloat(a.total_debit||a.debit||0);
const fcrd = a => parseFloat(a.total_credit||a.credit||0);
function pLabel(p){
if(!p) return '—';
if(/^Q[1-4]-\d{4}$/.test(p)){const q=p[1],y=p.split('-')[1];return`Q${q} ${['JanMar','AprJun','JulSep','OktDec'][q-1]} ${y}`;}
if(p.includes(':')){const[a,b]=p.split(':');return`${pLabel(a)}${pLabel(b)}`;}
const[y,m]=p.split('-');return`${MN[parseInt(m)-1]} ${y}`;
}
function pParam(p){
if(!p) return '';
if(/^Q[1-4]-\d{4}$/.test(p)){const q=parseInt(p[1])-1,y=p.split('-')[1];return`&period_from=${y}-${String(q*3+1).padStart(2,'0')}&period_to=${y}-${String(q*3+3).padStart(2,'0')}`;}
if(p.includes(':')){const[f,t]=p.split(':');return`&period_from=${f}&period_to=${t}`;}
return`&period=${p}`;
}
function curPeriod(){const d=new Date();return`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;}
// ═══════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════
async function api(path, method='GET', body){
const ctrl=new AbortController();
const t=setTimeout(()=>ctrl.abort(),10000);
try{
const h={'x-internal-source':'ouroboros-system','x-tenant-id':TENANT};
const o={headers:h,signal:ctrl.signal};
if(method!=='GET'){o.method=method;h['Content-Type']='application/json';o.body=JSON.stringify(body);}
const r=await fetch(path,o);
clearTimeout(t);
if(!r.ok) throw new Error(`HTTP ${r.status}`);
return await r.json();
}catch(e){clearTimeout(t);throw e;}
}
// ═══════════════════════════════════════════════════════════════════════
// STATUS
// ═══════════════════════════════════════════════════════════════════════
function setStatus(s){
const d=document.getElementById('sdot'),tx=document.getElementById('stext');
d.className='sdot';
if(s==='live'){d.classList.add('live');tx.textContent='Live';}
else if(s==='loading'){tx.textContent='';}
else if(s==='error'){d.classList.add('err');tx.textContent='Fel';}
}
// ═══════════════════════════════════════════════════════════════════════
// PERIOD MODE
// ═══════════════════════════════════════════════════════════════════════
function setPeriodMode(m){
S.periodMode=m;
['month','quarter','custom'].forEach(x=>{
document.getElementById('pmode-'+x).classList.toggle('active',x===m);
document.getElementById('pw-'+x).style.display=x===m?(m==='custom'?'flex':''):'none';
});
if(m==='month') applyMonth();
if(m==='quarter') applyQuarter();
}
function applyMonth(){S.period=document.getElementById('sel-month').value;loadAll();}
function applyQuarter(){S.period=document.getElementById('sel-quarter').value;loadAll();}
function applyCustom(){
const f=document.getElementById('p-from').value,t=document.getElementById('p-to').value;
if(!f||!t) return;if(f>t){alert('Från måste vara före Till');return;}
S.period=f+':'+t;loadAll();
}
function buildPeriodSelects(){
const ms=document.getElementById('sel-month');
ms.innerHTML='';
for(let m=1;m<=12;m++){
const v=`${FISCAL}-${String(m).padStart(2,'0')}`;
const o=document.createElement('option');o.value=v;o.textContent=`${MN[m-1]} ${FISCAL}`;
if(v===S.period)o.selected=true;ms.appendChild(o);
}
ms.onchange=applyMonth;
const qs=document.getElementById('sel-quarter');
qs.innerHTML='';
const curM=parseInt((S.period||'').split('-')[1]||6);
for(let q=1;q<=4;q++){
const o=document.createElement('option');
o.value=`Q${q}-${FISCAL}`;
o.textContent=`Q${q} ${['JanMar','AprJun','JulSep','OktDec'][q-1]} ${FISCAL}`;
if(Math.ceil(curM/3)===q)o.selected=true;
qs.appendChild(o);
}
qs.onchange=applyQuarter;
const pf=document.getElementById('p-from'),pt=document.getElementById('p-to');
if(!pf.value)pf.value=`${FISCAL}-01`;
if(!pt.value)pt.value=S.period||curPeriod();
}
// ═══════════════════════════════════════════════════════════════════════
// TAB
// ═══════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// MOBILNAVIGATION — Hamburgermeny & FAB
// ═══════════════════════════════════════════════════════════
function openMobDrawer(){
const d=document.getElementById('mob-drawer');
d.classList.add('open');
document.getElementById('ham-btn').setAttribute('aria-expanded','true');
document.body.style.overflow='hidden';
// Synka period-dropdown i drawer
const mainSel=document.getElementById('sel-month');
const drawerSel=document.getElementById('drawer-period-sel');
if(mainSel&&drawerSel){
drawerSel.innerHTML=mainSel.innerHTML;
drawerSel.value=mainSel.value;
}
// Synka rollnamn
const rn=document.getElementById('drawer-role-name');
if(rn&&obProfile&&obProfile.name) rn.textContent=obProfile.name+' — byt rolle';
}
function closeMobDrawer(){
const d=document.getElementById('mob-drawer');
d.classList.remove('open');
document.getElementById('ham-btn').setAttribute('aria-expanded','false');
document.body.style.overflow='';
}
function mobNavTo(t){
// Uppdatera aktiv i drawer
document.querySelectorAll('#mob-drawer-nav .drawer-nav-item').forEach(el=>{
el.classList.toggle('active', el.dataset.tab===t);
});
// Uppdatera indikator i topbaren
const labels={
overview:'📊 Översikt',transactions:'📋 Transaktioner',invoices:'🧾 Fakturor',
vat:'🏦 Skatt & Moms',payroll:'👥 Löner',period:'📅 Period',
tasks:'✅ Tasks',forecast:'📈 Prognos'
};
const cur=document.getElementById('mob-current-tab');
if(cur) cur.textContent=labels[t]||t;
closeMobDrawer();
switchTab(t);
}
function syncDrawerPeriod(val){
const mainSel=document.getElementById('sel-month');
if(mainSel&&mainSel.value!==val){
mainSel.value=val;
mainSel.dispatchEvent(new Event('change'));
}
}
// ── Kvittostatus-helpers ──
const EXPENSE_PFX=['4','5','6'];
const PAYROLL_PFX=['7'];
const INTERNAL_KW=['omvänd skatt','moms','justering','stämning','rättelse','avskrivning','överbetald','kursvinst','kursförlust','kursdiff','ingående balans','period close','opening'];
function needsReceipt(e){
if(e.source_type==='bank') return false;
const desc=(e.description||'').toLowerCase();
if(INTERNAL_KW.some(k=>desc.includes(k))) return false;
const accts=(e.lines||[]).map(l=>l.account_number||'');
return accts.some(a=>EXPENSE_PFX.includes(a[0])||PAYROLL_PFX.includes(a[0]));
}
function rcptBadge(e){
if(!needsReceipt(e)) return '<span style="font-size:.68rem;color:var(--muted)">☑ auto</span>';
const n=(e.metadata?.receipts||[]).length;
return n>0
?`<span style="font-size:.68rem;font-weight:700;color:var(--green)">✅ ${n} kvitto${n>1?'n':''}</span>`
:'<span style="font-size:.68rem;font-weight:700;color:var(--red)">❌ Saknar</span>';
}
// FAB — lägg till kvitto/utlägg
function fabAddReceipt(){
closeMobDrawer();
// Uppdatera indikatorer
document.querySelectorAll('#mob-drawer-nav .drawer-nav-item').forEach(el=>{
el.classList.toggle('active', el.dataset.tab==='transactions');
});
const cur=document.getElementById('mob-current-tab');
if(cur) cur.textContent='📋 Transaktioner';
// openNewEntry säkrar själv att rätt flik är aktiv
openNewEntry();
}
// Tangentbord: Escape stänger drawer
document.addEventListener('keydown',e=>{
if(e.key==='Escape') closeMobDrawer();
});
// ═══════════════════════════════════════════════════════════
function switchTab(t){
document.querySelectorAll('.tab-sec').forEach(s=>s.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
document.getElementById('tab-'+t).classList.add('active');
const idx={overview:0,transactions:1,invoices:2,vat:3,payroll:4,period:5,tasks:6,forecast:7};
const tabBtns=document.querySelectorAll('.tab-btn');
if(tabBtns[idx[t]??0]) tabBtns[idx[t]??0].classList.add('active');
// Uppdatera mob-indikator
const labels={
overview:'📊 Översikt',transactions:'📋 Transaktioner',invoices:'🧾 Fakturor',
vat:'🏦 Skatt & Moms',payroll:'👥 Löner',period:'📅 Period',
tasks:'✅ Tasks',forecast:'📈 Prognos'
};
const cur=document.getElementById('mob-current-tab');
if(cur) cur.textContent=labels[t]||t;
if(t==='transactions') txLoad(1);
if(t==='invoices') loadInvoices();
if(t==='vat') { loadVAT(); loadVATExtras(); }
if(t==='payroll') loadPayroll();
if(t==='period') loadPeriod();
if(t==='tasks') loadTasks();
if(t==='forecast') loadForecast();
}
// ═══════════════════════════════════════════════════════════════════════
// LOAD ALL (overview)
// ═══════════════════════════════════════════════════════════════════════
async function loadTasks(){
const el=document.getElementById('tasks-list');
if(!el) return;
el.innerHTML='<div style="padding:20px;text-align:center;color:#999">Laddar…</div>';
try{
const r=await api('/api/ledger/tasks');
if(!r.ok){el.innerHTML='<div style="color:red">Fel: '+r.error+'</div>';return;}
window._lastTasks = r.tasks || [];
// Info-rad (kassa/burn) — alltid synlig på overview
const infoTask = r.tasks.find(t=>t.id==='C011-info');
const badgeCard=document.getElementById('tasks-badge-card');
const badgeBody=document.getElementById('tasks-badge-body');
const badgeTitle=document.getElementById('tasks-badge-title');
if(badgeCard){
const actionable = r.tasks.filter(t=>t.severity!=='info');
if(actionable.length>0){
badgeCard.style.display='';
const hasCrit = actionable.some(t=>t.severity==='critical');
badgeTitle.innerHTML=(hasCrit?'⛔':'⚠️')+' <strong>'+actionable.length+'</strong> åtgärd'+(actionable.length>1?'er':'')+' kräver handling';
badgeBody.innerHTML=actionable.slice(0,4).map(t=>{
const col=t.severity==='critical'?'#FF3B30':t.severity==='high'?'#FF9500':'#333';
const sym=t.severity==='critical'?'⛔':t.severity==='high'?'⚠️':'️';
return '<div style="margin:3px 0;color:'+col+'">'+sym+' '+t.title+'</div>';
}).join('');
} else {
badgeCard.style.display='none';
}
}
// tasks-tab badge
const taskBtn=document.getElementById('tasks-tab-btn');
const actionCount=r.tasks.filter(t=>t.severity!=='info').length;
if(taskBtn) taskBtn.textContent='✅ Tasks'+(actionCount>0?' ('+actionCount+')':'');
// Bygg tasks-lista
const sevOrder={critical:0,high:1,medium:2,info:3};
const sorted=r.tasks.slice().sort((a,b)=>(sevOrder[a.severity]??9)-(sevOrder[b.severity]??9));
// Kassa-info-panel
const infoRow = infoTask ? `<div style="background:#F2F2F7;border-radius:10px;padding:12px 16px;margin-bottom:12px;display:flex;flex-wrap:wrap;gap:16px">
${infoTask.description.split('.').filter(Boolean).map(s=>'<span style="font-size:.8rem;color:#444">'+s.trim()+'</span>').join('<span style="color:#ccc"> | </span>')}
</div>` : '';
const actionItems = sorted.filter(t=>t.severity!=='info');
if(actionItems.length===0){
el.innerHTML=infoRow+'<div class="tasks-ok">✅ Inga öppna åtgärder</div>';
return;
}
el.innerHTML = infoRow + actionItems.map(t=>{
const col={critical:'#FF3B30',high:'#FF9500',medium:'#FFCC00'}[t.severity]||'#8E8E93';
const label={critical:'⛔ Kritisk',high:'⚠️ Hög prioritet',medium:'️ Medium'}[t.severity]||t.severity;
const amt=t.amount?'<div style="font-size:.78rem;font-weight:700;margin-top:4px">'+Math.round(t.amount).toLocaleString('sv-SE')+' kr</div>':'';
const link=t.action_url?'<a href="'+t.action_url+'" target="_blank" style="background:#007AFF;color:#fff;border-radius:6px;padding:5px 14px;font-size:.75rem;text-decoration:none;white-space:nowrap;flex-shrink:0">Åtgärda →</a>':'';
return '<div class="task-item '+t.severity+'" style="border-left:4px solid '+col+'">'+
'<div style="flex:1">'+
'<div style="font-size:.7rem;font-weight:700;color:'+col+';text-transform:uppercase;margin-bottom:3px">'+label+'</div>'+
'<div style="font-size:.84rem;font-weight:600;color:#1c1c1e">'+t.title+'</div>'+
'<div style="font-size:.76rem;color:#666;margin-top:3px">'+t.description+'</div>'+
(t.period?'<div style="font-size:.74rem;color:#999;margin-top:2px">📅 '+t.period+'</div>':'')+
amt+'</div>'+link+'</div>';
}).join('');
}catch(e){el.innerHTML='<div style="color:red">Fel: '+e.message+'</div>';}
}
async function loadAll(){
setStatus('loading');
buildPeriodSelects();
loadTasks();
try{
const pp=pParam(S.period);
const[acR,tbR,jR,prR]=await Promise.all([
api(`/api/ledger/accounts`),
api(`/api/ledger/trial-balance?fiscal_year=${FISCAL}${pp}`),
api(`/api/ledger/journal?fiscal_year=${FISCAL}${pp}&limit=50&status=posted`),
api(`/api/ledger/periods?fiscal_year=${FISCAL}`),
]);
S.accounts=acR.accounts||[];
S.tb=tbR.accounts||[];
S.journal=jR.entries||[];
S.periods=prR.periods||[];
allAccounts=S.accounts;
setStatus('live');
renderOverview();
}catch(e){
console.warn('Load failed:',e.message);
setStatus('error');
}
}
// ═══════════════════════════════════════════════════════════════════════
// KPI DRILL-DOWN MODALER
// ═══════════════════════════════════════════════════════════════════════
function fmtRow(label, amount, sub='', color='') {
const col = color ? `color:${color};` : '';
return `<div style="display:flex;justify-content:space-between;align-items:baseline;padding:7px 0;border-bottom:1px solid #F2F2F7">
<span style="font-size:.83rem;color:#444">${label}${sub?'<div style="font-size:.72rem;color:#999;margin-top:1px">'+sub+'</div>':''}</span>
<span style="font-size:.88rem;font-weight:700;${col}white-space:nowrap;margin-left:12px">${fmtAmt(amount)}</span>
</div>`;
}
function fmtAmt(n) {
if(n==null||isNaN(n)) return '—';
return Math.round(n).toLocaleString('sv-SE') + ' kr';
}
function totalRow(label, amount, color='') {
const col = color ? `color:${color};` : '';
return `<div style="display:flex;justify-content:space-between;align-items:baseline;padding:9px 0;margin-top:4px;border-top:2px solid #1c1c1e">
<span style="font-size:.85rem;font-weight:700">${label}</span>
<span style="font-size:.92rem;font-weight:800;${col}">${fmtAmt(amount)}</span>
</div>`;
}
function showKassaModal() {
const tb = S.tb;
const accounts = tb.filter(a => /^19/.test(a.account_number));
const kassaMap = {
'1930': 'Företagskonto Nordea',
'1932': 'Revolut SEK',
'1933': 'Revolut GBP',
'1934': 'Revolut EUR',
'1935': 'Revolut USD',
'1936': 'Revolut THB',
};
const skulder2650 = Math.max(0, -(parseFloat(tb.find(a=>a.account_number==='2650')?.balance||0)));
const skulder2710 = Math.max(0, -(parseFloat(tb.find(a=>a.account_number==='2710')?.balance||0)));
const skulder2731 = Math.max(0, -(parseFloat(tb.find(a=>a.account_number==='2731')?.balance||0)));
const totalSkulder = skulder2650 + skulder2710 + skulder2731;
const totalKassa = accounts.reduce((s,a)=>s+parseFloat(a.balance||0),0);
const netto = totalKassa - totalSkulder;
let html = '<div style="font-size:.78rem;color:#999;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Likvida medel</div>';
for (const a of accounts) {
const bal = parseFloat(a.balance||0);
if (Math.abs(bal) < 1) continue;
const name = kassaMap[a.account_number] || a.account_name || a.account_number;
html += fmtRow(name, bal, a.account_number);
}
html += totalRow('Total kassa', totalKassa);
html += '<div style="font-size:.78rem;color:#999;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin:16px 0 8px">Förfallna skulder (dras av)</div>';
if (skulder2650 > 0) html += fmtRow('Momsdeklaration (2650)', -skulder2650, 'Betala till Skatteverket', '#FF3B30');
if (skulder2710 > 0) html += fmtRow('Personalskatt (2710)', -skulder2710, 'Betala till Skatteverket', '#FF3B30');
if (skulder2731 > 0) html += fmtRow('Arbetsgivaravgift (2731)', -skulder2731, 'Betala till Skatteverket', '#FF9500');
if (totalSkulder === 0) html += '<div style="color:#34C759;font-size:.82rem;padding:8px 0">✅ Inga förfallna skulder</div>';
html += totalRow('Kassa efter skulder', netto, netto >= 0 ? '#34C759' : '#FF3B30');
openInfo('🏦 Kassa & likviditet', html);
}
function showSkuldModal() {
const tb = S.tb;
const skuldKonton = [
{ nr: '2650', namn: 'Momsdeklaration', not: 'Betala till Skatteverket', sev: 'critical' },
{ nr: '2710', namn: 'Personalskatt', not: 'Betala till Skatteverket senast 12:e', sev: 'critical' },
{ nr: '2731', namn: 'Arbetsgivaravgift', not: 'Betala tillsammans med personalskatt', sev: 'critical' },
{ nr: '2611', namn: 'Utgående moms 25% (ej deklarerad)', not: 'Kvar i 2611, ej omförd till 2650', sev: 'high' },
{ nr: '2615', namn: 'Moms omvänd skattskyldighet', not: 'Matchar 2645 — netto 0', sev: 'info' },
];
let html = '<div style="font-size:.78rem;color:#999;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Skatteskulder</div>';
let total = 0;
for (const k of skuldKonton) {
const a = tb.find(x => x.account_number === k.nr);
const skuld = a ? Math.max(0, -parseFloat(a.balance||0)) : 0;
if (skuld < 1 && k.sev !== 'info') continue;
const col = k.sev==='critical'?'#FF3B30':k.sev==='high'?'#FF9500':'#8E8E93';
html += fmtRow(k.namn, skuld>0?-skuld:0, k.not, skuld>0?col:'#34C759');
if (k.sev !== 'info') total += skuld;
}
html += totalRow('Totalt att betala nu', -total, total>0?'#FF3B30':'#34C759');
html += '<div style="font-size:.78rem;color:#999;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin:16px 0 8px">Övriga skulder</div>';
const ovriga = tb.filter(a => a.account_number.startsWith('2') && !['2650','2710','2731','2611','2615','2645','2099'].includes(a.account_number));
for (const a of ovriga) {
const skuld = Math.max(0, -parseFloat(a.balance||0));
if (skuld < 100) continue;
html += fmtRow(a.account_name||a.account_number, -skuld, a.account_number);
}
openInfo('🧾 Skulder — fullständig lista', html);
}
function showIntakterModal() {
const tb = S.tb;
const intakter = tb.filter(a => a.account_number.startsWith('3'));
let html = '<div style="font-size:.78rem;color:#999;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Intäktskonton</div>';
let total = 0;
for (const a of intakter.sort((x,y)=>x.account_number.localeCompare(y.account_number))) {
const belopp = Math.abs(parseFloat(a.balance||0));
if (belopp < 1) continue;
html += fmtRow(a.account_name||a.account_number, belopp, a.account_number);
total += belopp;
}
html += totalRow('Totala intäkter', total, '#34C759');
openInfo('📈 Intäkter — detalj', html);
}
function showKostnaderModal() {
const tb = S.tb;
const klass = {'4':'Varor & material','5':'Lokalkostnader, resor','6':'Externa tjänster, IT','7':'Personalkostnader','8':'Finansiella'};
let html = '';
let grandTotal = 0;
for (const [k, rubrik] of Object.entries(klass)) {
const konton = tb.filter(a => a.account_number.startsWith(k));
const klassTotal = konton.reduce((s,a)=>s+Math.max(0,parseFloat(a.balance||0)),0);
if (klassTotal < 1) continue;
html += `<div style="font-size:.78rem;color:#999;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin:${html?'14px':0} 0 6px">${rubrik}</div>`;
for (const a of konton.sort((x,y)=>Math.abs(parseFloat(y.balance||0))-Math.abs(parseFloat(x.balance||0)))) {
const kost = Math.max(0, parseFloat(a.balance||0));
if (kost < 100) continue;
html += fmtRow(a.account_name||a.account_number, kost, a.account_number);
}
grandTotal += klassTotal;
}
html += totalRow('Totala kostnader', grandTotal, '#FF3B30');
openInfo('📉 Kostnader — detalj', html);
}
function showResultatModal() {
const tb = S.tb;
const intakter = tb.filter(a=>a.account_number.startsWith('3')).reduce((s,a)=>s+Math.abs(parseFloat(a.balance||0)),0);
const kostnader = tb.filter(a=>/^[4-8]/.test(a.account_number)).reduce((s,a)=>s+Math.max(0,parseFloat(a.balance||0)),0);
const resultat = intakter - kostnader;
const skatt = Math.max(0, resultat * 0.206);
const efterSkatt = resultat - skatt;
const html =
fmtRow('Intäkter (klass 3)', intakter, 'Fakturerad omsättning', '#34C759') +
fmtRow('Rörelsekostnader (klass 4-6)', -kostnader, 'Varor, resor, IT, tjänster') +
fmtRow('Personalkostnader (klass 7)', 0, 'Ingår i rörelsekostnader') +
totalRow('Rörelseresultat', resultat, resultat>=0?'#34C759':'#FF3B30') +
'<div style="margin-top:12px"></div>' +
fmtRow('Bolagsskatt 20,6% (beräknad)', resultat>0?-skatt:0, 'Ej bokförd') +
totalRow('Resultat efter skatt (est.)', resultat>0?efterSkatt:resultat, resultat>0&&efterSkatt>=0?'#34C759':'#FF3B30');
openInfo('💰 Resultat — detalj', html);
}
// ═══════════════════════════════════════════════════════════════════════
// OVERVIEW RENDER
// ═══════════════════════════════════════════════════════════════════════
function renderOverview(){
const tb=S.tb;
const cash =tb.filter(a=>/^19/.test(a.account_number)).reduce((s,a)=>s+fbal(a),0);
const revenue =tb.filter(a=>/^3/.test(a.account_number)).reduce((s,a)=>s+Math.abs(fbal(a)),0);
const expenses=tb.filter(a=>/^[4-8]/.test(a.account_number)).reduce((s,a)=>s+Math.abs(fbal(a)),0);
const net=revenue-expenses;
// Moms: 2611 kredit - 2645 debet
const out2611=Math.abs(fbal(tb.find(a=>a.account_number==='2611')||{}));
const in2645 =Math.abs(fbal(tb.find(a=>a.account_number==='2645')||{}));
const vat=out2611-in2645;
function kpi(id,valId,v,color){
document.getElementById(id).className=`kpi ${color}`;
const el=document.getElementById(valId);el.className=`kpi-val ${color}`;el.textContent=fmt(v);
}
kpi('kpi-cash','kpi-cash-v',cash,cash>=0?'green':'red');
kpi('kpi-rev','kpi-rev-v',revenue,'amber');
kpi('kpi-exp','kpi-exp-v',expenses,'navy');
kpi('kpi-net','kpi-net-v',net,net>=0?'green':'red');
document.getElementById('kpi-vat-v').textContent=fmt(vat);
document.getElementById('pl-badge').textContent=pLabel(S.period);
document.getElementById('ft-period').textContent=pLabel(S.period);
document.getElementById('ft-count').textContent=S.journal.length?`${S.journal.length} verifikat (sida 1)`:'';
renderPL(tb,revenue,expenses,net);
renderJW();
renderPeriodBadge();
loadVATExtras();
}
function renderPL(tb,rev,exp,net){
const body=document.getElementById('pl-body');
const revA=tb.filter(a=>/^3/.test(a.account_number)&&Math.abs(fbal(a))>0.005);
const expA=tb.filter(a=>/^[4-8]/.test(a.account_number)&&Math.abs(fbal(a))>0.005);
if(rev===0&&exp===0){body.innerHTML=`<div style="text-align:center;padding:30px;color:var(--muted)">📭 Inga transaktioner denna period</div>`;return;}
let h='<div style="margin-bottom:16px"><div class="pl-sec-title">Intäkter</div>';
revA.forEach(a=>{h+=`<div class="pl-row"><span class="pl-acct">${esc(acname(a))}<span class="pl-code">${a.account_number}</span></span><span class="pl-amt">${fmt(Math.abs(fbal(a)))}</span></div>`;});
h+=`<div class="pl-total"><span class="pl-total-lbl">SUMMA INTÄKTER</span><span class="pl-total-amt" style="color:var(--green)">${fmt(rev)}</span></div></div>`;
h+='<div style="margin-bottom:10px"><div class="pl-sec-title">Kostnader</div>';
expA.forEach(a=>{h+=`<div class="pl-row"><span class="pl-acct">${esc(acname(a))}<span class="pl-code">${a.account_number}</span></span><span class="pl-amt">${fmt(Math.abs(fbal(a)))}</span></div>`;});
h+=`<div class="pl-total"><span class="pl-total-lbl">SUMMA KOSTNADER</span><span class="pl-total-amt">${fmt(exp)}</span></div></div>`;
h+=`<div class="pl-result ${net>=0?'pos':'neg'}"><span class="pl-result-lbl">🏁 Rörelseresultat</span><span class="pl-result-amt" style="color:${net>=0?'var(--green)':'var(--red)'}">${net>=0?'+':''}${fmt(net)}</span></div>`;
body.innerHTML=h;
}
function renderJW(){
const b=document.getElementById('jw-body');
const en=[...S.journal].sort((a,c)=>new Date(c.entry_date||c.date)-new Date(a.entry_date||a.date)).slice(0,8);
if(!en.length){b.innerHTML='<div style="text-align:center;padding:14px;color:var(--muted);font-size:.82rem">Inga verifikat denna period</div>';return;}
b.innerHTML=en.map(e=>{
const d=(e.entry_date||e.date||'').toString().slice(0,10);
const nr=e.entry_number||'—';
const ls=e.lines||[];
const amt=ls.reduce((s,l)=>s+parseFloat(l.debit||0),0);
const isBank=e.source_type==='bank';
const jwMiss=needsReceipt(e)&&!(e.metadata?.receipts||[]).length;
return`<div class="jw-row" onclick="openEntryModal('${e.id||''}')" style="${jwMiss?'border-left:3px solid var(--red);padding-left:9px':''}">
<span class="jw-nr">#${nr}</span>
<span class="jw-date">${d}</span>
<span class="jw-desc">${esc(e.description||'')}${isBank?'<span class="tag bank">BANK</span>':''}${jwMiss?'<span style="font-size:.65rem;font-weight:700;color:var(--red);margin-left:4px">❌</span>':''}</span>
<span class="jw-amt">${amt>0?fmt(amt):'—'}</span>
</div>`;
}).join('');
}
function renderPeriodBadge(){
const p=S.periods.find(x=>x.period===S.period);
const st=p?.status||'open';
const lbl={open:'Öppen',review:'Under granskning',approved:'Godkänd',closed:'Stängd'};
const cls={open:'open',review:'review',approved:'review',closed:'closed'};
const b=document.getElementById('ov-period-badge');
b.className=`pbadge ${cls[st]||'open'}`;b.textContent=`${lbl[st]||st}`;
document.getElementById('ov-period-detail').textContent=
{open:'Period öppen för bokföring',review:'Inväntar granskning',approved:'Godkänd av revisor',closed:'Stängd — inga ändringar'}[st]||'';
}
// ═══════════════════════════════════════════════════════════════════════
// BALANSRÄKNING MODAL
// ═══════════════════════════════════════════════════════════════════════
function showBS(){
const tb=S.tb;
const assets=tb.filter(a=>/^1/.test(a.account_number)&&Math.abs(fbal(a))>0.005);
const equity=tb.filter(a=>/^2/.test(a.account_number)&&Math.abs(fbal(a))>0.005);
const tA=assets.reduce((s,a)=>s+fbal(a),0);
const tE=equity.reduce((s,a)=>s+Math.abs(fbal(a)),0);
const diff=Math.abs(tA-tE);
function rows(arr,abs){return arr.map(a=>`<tr><td style="font-family:monospace;font-size:.78rem">${a.account_number}</td><td>${esc(acname(a))}</td><td style="text-align:right;font-variant-numeric:tabular-nums">${fmt(abs?Math.abs(fbal(a)):fbal(a))}</td></tr>`).join('');}
const h=`<table style="width:100%;border-collapse:collapse;font-size:.83rem">
<thead><tr style="border-bottom:2px solid var(--border)"><th style="padding:4px 6px;text-align:left;font-size:.68rem;color:var(--muted)">Konto</th><th style="padding:4px 6px;text-align:left;font-size:.68rem;color:var(--muted)">Namn</th><th style="padding:4px 6px;text-align:right;font-size:.68rem;color:var(--muted)">Saldo</th></tr></thead><tbody>
<tr><td colspan="3" style="padding:8px 6px 3px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)">TILLGÅNGAR</td></tr>
${rows(assets,false)}
<tr style="background:var(--navy-05)"><td colspan="2" style="padding:6px;font-weight:700">SUMMA TILLGÅNGAR</td><td style="padding:6px;text-align:right;font-weight:800;color:var(--green)">${fmt(tA)}</td></tr>
<tr><td colspan="3" style="padding:10px 6px 3px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)">EGET KAPITAL &amp; SKULDER</td></tr>
${rows(equity,true)}
<tr style="background:var(--navy-05)"><td colspan="2" style="padding:6px;font-weight:700">SUMMA EK &amp; SKULDER</td><td style="padding:6px;text-align:right;font-weight:800;${diff<1?'color:var(--green)':'color:var(--red)'}">${fmt(tE)}</td></tr>
</tbody></table>
${diff<1?'<div style="margin-top:10px;padding:8px 12px;background:rgba(39,174,96,.08);border:1px solid rgba(39,174,96,.2);border-radius:7px;font-size:.8rem;color:var(--green)">✅ Balansräkningen balanserar</div>':'<div style="margin-top:10px;padding:8px 12px;background:rgba(229,62,62,.07);border:1px solid rgba(229,62,62,.2);border-radius:7px;font-size:.8rem;color:var(--red)">⚠️ Differens: '+fmt(tA-tE)+'</div>'}`;
openInfo(`📊 Balansräkning — ${pLabel(S.period)}`,h);
}
// ═══════════════════════════════════════════════════════════════════════
// TRANSAKTIONER
// ═══════════════════════════════════════════════════════════════════════
const TX_LIMIT=50;
let txSearchTimer=null;
function txSearchDebounce(){clearTimeout(txSearchTimer);txSearchTimer=setTimeout(()=>txLoad(1),350);}
async function txLoad(page){
S.txPage=page||1;
S.txSearch=document.getElementById('tx-search').value.trim();
S.txSrc=document.getElementById('tx-src').value;
S.txStatus=document.getElementById('tx-status-f').value;
const pp=pParam(S.period);
const offset=(S.txPage-1)*TX_LIMIT;
let url=`/api/ledger/journal?fiscal_year=${FISCAL}${pp}&limit=${TX_LIMIT}&offset=${offset}`;
if(S.txSrc) url+=`&source_type=${S.txSrc}`;
if(S.txStatus) url+=`&status=${S.txStatus}`;
if(S.txSearch) url+=`&search=${encodeURIComponent(S.txSearch)}`;
const tbody=document.getElementById('tx-tbody');
const tcards=document.getElementById('tx-cards');
tbody.innerHTML='<tr><td colspan="7" class="tx-empty">Laddar…</td></tr>';
tcards.innerHTML='<div class="tx-card-empty">Laddar…</div>';
try{
const r=await api(url);
if(!r.ok && !r.entries){
const msg=r.error||'Kunde inte hämta transaktioner';
tbody.innerHTML=`<tr><td colspan="7" class="tx-empty">⚠️ ${msg}</td></tr>`;
tcards.innerHTML=`<div class="tx-card-empty">⚠️ ${msg}</div>`;
updatePager();return;
}
const entries=r.entries||[];
S.txTotal=r.total||r.count||entries.length;
document.getElementById('tx-count-badge').textContent=`${S.txTotal} verifikat`;
if(!entries.length){
tbody.innerHTML='<tr><td colspan="7" class="tx-empty">Inga verifikat matchar filtret</td></tr>';
tcards.innerHTML='<div class="tx-card-empty">Inga verifikat matchar filtret</div>';
updatePager();return;
}
// ── Desktop: tabellrader ──
tbody.innerHTML=entries.map(e=>{
const d=(e.entry_date||e.date||'').toString().slice(0,10);
const nr=e.entry_number||'—';
const lines=e.lines||[];
const amt=lines.reduce((s,l)=>s+parseFloat(l.debit||0),0);
const accts=[...new Set(lines.map(l=>l.account_number))].slice(0,3).join(', ');
const srcCls=e.source_type==='bank'?'src-bank':'src-manual';
const srcLbl=e.source_type==='bank'?'BANK':'MAN';
const stBadge=e.status==='posted'?'<span style="color:var(--green);font-size:.7rem;font-weight:700">✓ Bokförd</span>':
e.status==='void'?'<span style="color:var(--red);font-size:.7rem">✗ Makulerad</span>':
'<span style="color:var(--amber);font-size:.7rem">⏳ Utkast</span>';
const trBg=needsReceipt(e)&&!(e.metadata?.receipts||[]).length?'background:rgba(229,62,62,.05)':'';
return`<tr onclick="openEntryModal('${e.id}')" style="${trBg}">
<td><span class="tx-nr">#${nr}</span></td>
<td style="white-space:nowrap">${d}</td>
<td style="max-width:280px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis" title="${esc(e.description||'')}">${esc(e.description||'')}</td>
<td><span class="${srcCls}">${srcLbl}</span></td>
<td style="font-size:.75rem;color:var(--muted)">${esc(accts)}</td>
<td class="tx-amt">${amt>0?fmt(amt):'—'}</td>
<td style="text-align:center">${rcptBadge(e)}</td>
</tr>`;
}).join('');
// ── Mobil: kort ──
tcards.innerHTML=entries.map(e=>{
const d=(e.entry_date||e.date||'').toString().slice(0,10).slice(5); // MM-DD
const dFull=(e.entry_date||e.date||'').toString().slice(0,10);
const nr=e.entry_number||'—';
const lines=e.lines||[];
const amt=lines.reduce((s,l)=>s+parseFloat(l.debit||0),0);
const accts=[...new Set(lines.map(l=>l.account_number))].slice(0,4).join(' · ');
const isBank=e.source_type==='bank';
const stColor=e.status==='posted'?'var(--green)':e.status==='void'?'var(--red)':'var(--amber)';
const stText=e.status==='posted'?'✓ Bokförd':e.status==='void'?'✗ Makulerad':'⏳ Utkast';
const cardBorder=needsReceipt(e)&&!(e.metadata?.receipts||[]).length?'border-left:3px solid var(--red)':'';
return`<div class="tx-card" onclick="openEntryModal('${e.id}')" style="${cardBorder}">
<div class="tx-card-left">
<div class="tx-card-nr">#${nr}</div>
<div class="tx-card-src-dot${isBank?' bank':''}"></div>
</div>
<div class="tx-card-body">
<div class="tx-card-desc">${esc(e.description||'—')}</div>
<div class="tx-card-meta">
<span>${dFull}</span>
${accts?`<span class="tx-card-accts">${esc(accts)}</span>`:''}
${rcptBadge(e)}
</div>
</div>
<div class="tx-card-right">
<div class="tx-card-amt">${amt>0?fmt(amt):'—'}</div>
<div class="tx-card-status" style="color:${stColor}">${stText}</div>
</div>
</div>`;
}).join('');
updatePager();
}catch(err){
tbody.innerHTML=`<tr><td colspan="7" class="tx-empty">⚠️ Fel: ${err.message}</td></tr>`;
tcards.innerHTML=`<div class="tx-card-empty">⚠️ Fel: ${err.message}</div>`;
}
}
function updatePager(){
const pages=Math.ceil(S.txTotal/TX_LIMIT)||1;
const start=(S.txPage-1)*TX_LIMIT+1;
const end=Math.min(S.txPage*TX_LIMIT,S.txTotal);
document.getElementById('pg-info').textContent=`Visar ${start}${end} av ${S.txTotal}`;
document.getElementById('pg-prev').disabled=S.txPage<=1;
document.getElementById('pg-next').disabled=S.txPage>=pages;
}
function txPage(d){txLoad(S.txPage+d);}
// ═══════════════════════════════════════════════════════════════════════
// ENTRY DETAIL MODAL
// ═══════════════════════════════════════════════════════════════════════
// Cache hämtade entries
const entryCache={};
async function openEntryModal(id){
if(!id) return;
// Kolla om vi har den i journal-state
let e=S.journal.find(j=>j.id===id);
if(!e&&entryCache[id]) e=entryCache[id];
if(!e){
// Hämta via search (vi har inget GET /journal/:id endpoint — använd filter)
try{
const r=await api(`/api/ledger/journal?fiscal_year=${FISCAL}&limit=1&offset=0`);
// Fallback: visa direkt från DOM-data
}catch(err){}
}
if(!e){openInfo('Verifikat','Detaljer ej tillgängliga för detta verifikat.');return;}
entryCache[id]=e;
const d=(e.entry_date||e.date||'').toString().slice(0,10);
const nr=e.entry_number||'—';
const lines=e.lines||[];
const deb=lines.reduce((s,l)=>s+parseFloat(l.debit||0),0);
const crd=lines.reduce((s,l)=>s+parseFloat(l.credit||0),0);
const meta=e.metadata||{};
let h=`<div style="margin-bottom:14px;padding:10px 12px;background:var(--navy-05);border-radius:7px">
<div style="display:grid;grid-template-columns:auto 1fr;gap:4px 12px;font-size:.8rem">
<span style="color:var(--muted)">Datum:</span><strong>${d}</strong>
<span style="color:var(--muted)">Verifikat:</span><strong>#${nr}</strong>
<span style="color:var(--muted)">Status:</span><strong>${e.status||'—'}</strong>
<span style="color:var(--muted)">Källa:</span><strong>${e.source_type||'—'}</strong>
${e.reference?`<span style="color:var(--muted)">Referens:</span><strong>${esc(e.reference)}</strong>`:''}
${e.user_id&&e.user_id!=='system'?`<span style="color:var(--muted)">Bokfört av:</span><strong>${esc(e.user_id)}</strong>`:''}
</div>
</div>
<table class="det-table">
<thead><tr><th>Konto</th><th>Namn</th><th style="text-align:right">Debet</th><th style="text-align:right">Kredit</th></tr></thead>
<tbody>`;
lines.forEach(l=>{
h+=`<tr><td style="font-family:monospace;font-size:.78rem">${l.account_number||''}</td>
<td>${esc(l.account_name||'')}</td>
<td style="text-align:right;font-variant-numeric:tabular-nums">${parseFloat(l.debit||0)>0?fmt(parseFloat(l.debit)):''}</td>
<td style="text-align:right;font-variant-numeric:tabular-nums">${parseFloat(l.credit||0)>0?fmt(parseFloat(l.credit)):''}</td></tr>`;
});
h+=`</tbody><tfoot><tr class="det-total">
<td colspan="2">SUMMA</td>
<td style="text-align:right;font-variant-numeric:tabular-nums">${fmt(deb)}</td>
<td style="text-align:right;font-variant-numeric:tabular-nums">${fmt(crd)}</td>
</tr></tfoot></table>`;
// Metadata
if(Object.keys(meta).length>0){
h+=`<div class="det-meta"><strong>Metadata:</strong><br>`;
Object.entries(meta).forEach(([k,v])=>{h+=`<span style="display:inline-block;margin-right:10px;margin-top:3px"><strong>${esc(k)}:</strong> ${esc(typeof v==='object'?JSON.stringify(v):String(v))}</span>`;});
h+=`</div>`;
}
const rcptCount = (e.metadata?.receipts || []).length;
h+=`<div style="margin-top:10px;display:flex;gap:8px;align-items:center">
<button onclick="closeModal('modal-info');openReceiptModal('${e.id}','${esc(e.description||'').replace(/'/g,"\'")}','${e.entry_number||'?'}')" class="act-btn primary" style="font-size:.8rem">
📎 ${rcptCount > 0 ? rcptCount + ' kvitto' + (rcptCount>1?'n':'') : 'Lägg till kvitto'}
</button>
${rcptCount > 0 ? '<span style="font-size:.75rem;color:var(--green)">✓ Kvitton uppladdade</span>' : '<span style="font-size:.75rem;color:var(--muted)">Inget kvitto ännu</span>'}
</div>`;
openInfo(`Verifikat #${nr}${esc(e.description||'').slice(0,50)}`,h);
}
// ═══════════════════════════════════════════════════════════════════════
// FAKTUROR
// ═══════════════════════════════════════════════════════════════════════
async function loadInvoices(){
const empty=document.getElementById('inv-empty');
const table=document.getElementById('inv-table');
empty.textContent='Laddar fakturor…';empty.style.display='';table.style.display='none';
try{
const r=await api(`/api/ledger/invoices?fiscal_year=${FISCAL}`);
const invs=(r.invoices||[]).filter(i=>{
const d=(i.description||'').toLowerCase();
return !d.includes('växling')&&!d.includes('[revolut')&&(i.total||0)>=1000;
});
S.invoices=invs;
const tot =invs.reduce((s,i)=>s+(i.total||0),0);
const paid=invs.filter(i=>i.status==='paid').reduce((s,i)=>s+(i.total||0),0);
const pend=invs.filter(i=>['sent','overdue'].includes(i.status)).reduce((s,i)=>s+(i.total||0),0);
document.getElementById('inv-summary').innerHTML=`
<div class="vat-card"><div class="vat-ruta">Totalt fakturerat</div><div class="vat-amt">${fmt(tot)}</div></div>
<div class="vat-card"><div class="vat-ruta">Betalt</div><div class="vat-amt" style="color:var(--green)">${fmt(paid)}</div></div>
<div class="vat-card"><div class="vat-ruta">Utestående</div><div class="vat-amt" style="${pend>0?'color:var(--amber)':'color:var(--muted)'}">${fmt(pend)}</div></div>`;
if(!invs.length){empty.innerHTML='🧾 Inga fakturor ännu. Klicka "Ny faktura" för att börja.';return;}
empty.style.display='none';table.style.display='';
const sl={draft:'Utkast',sent:'Skickad',paid:'Betald',overdue:'Förfallen',posted:'Bokförd'};
document.getElementById('inv-tbody').innerHTML=invs.map(i=>{
const cls=i.status||'posted';
const tot=i.total||i.totalInclVat||0;
const exc=i.amount||(tot/1.25);
const vat=i.vatAmount||(tot-exc);
return`<tr onclick="openEntryModal('${i.id||''}')">
<td><strong>#${i.entry_number||i.number||'—'}</strong><br><span style="font-size:.7rem;color:var(--muted)">${esc(i.reference||'')}</span></td>
<td><strong>${esc(i.customer||'—')}</strong></td>
<td style="white-space:nowrap">${i.date||'—'}</td>
<td style="max-width:200px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${esc(i.description||'')}</td>
<td style="text-align:right;font-variant-numeric:tabular-nums">${fmt(exc)}</td>
<td style="text-align:right;font-variant-numeric:tabular-nums">${fmt(vat)}</td>
<td style="text-align:right;font-weight:700;font-variant-numeric:tabular-nums">${fmt(tot)}</td>
<td><span class="inv-status ${cls}">● ${sl[cls]||cls}</span></td>
</tr>`;
}).join('');
}catch(err){empty.innerHTML=`⚠️ Fel: ${err.message}`;}
}
// ═══════════════════════════════════════════════════════════════════════
// MOMS
// ═══════════════════════════════════════════════════════════════════════
async function loadVAT(){
const body=document.getElementById('vat-body');
body.innerHTML='<div style="text-align:center;padding:20px;color:var(--muted)">Hämtar momsdata…</div>';
const q=document.getElementById('vat-q-sel').value;
const qi=parseInt(q[1])-1;
const y=FISCAL;
const from=`${y}-${String(qi*3+1).padStart(2,'0')}`;
const to=`${y}-${String(qi*3+3).padStart(2,'0')}`;
try{
const r=await api(`/api/ledger/trial-balance?fiscal_year=${FISCAL}&period_from=${from}&period_to=${to}`);
const accs=r.accounts||[];
const g=(num,field)=>{const a=accs.find(x=>x.account_number===num);return a?Math.abs(parseFloat(a[field]||0)):0;};
const out25 =g('2611','total_credit');
const outOSS=g('2615','total_credit');
const in2645=g('2645','total_debit');
const rev =accs.filter(a=>/^3/.test(a.account_number)).reduce((s,a)=>s+Math.abs(fbal(a)),0);
const netto =out25+outOSS-in2645;
// Q1 moms obetald-varning (hårdkodad känd fakta från bokföringen)
const q1warn=q==='Q1'||q==='Q2';
let h='';
if(q1warn&&q==='Q2'){
h+=`<div class="vat-alert warn">⚠️ <strong>OBS: Q1-moms 202 787 kr är obetald</strong> — förfallen till Skatteverket. Åtgärda omgående.</div>`;
}
const rutor=[
{r:'05',lbl:'Momspliktig omsättning (exkl. moms)',amt:rev,note:'Konton 3000/3010/3020'},
{r:'10',lbl:'Utgående moms 25%',amt:out25,note:'Konto 2611'},
{r:'20',lbl:'Utgående moms omvänd skattskyldighet',amt:outOSS,note:'Konto 2615'},
{r:'30',lbl:'Ingående moms avdragsgill',amt:in2645,note:'Konto 2645'},
];
h+=`<div class="vat-grid">`;
rutor.forEach(rv=>{
h+=`<div class="vat-card">
<div class="vat-ruta">Ruta ${rv.r}</div>
<div class="vat-lbl">${rv.lbl}</div>
<div class="vat-amt" ${!rv.amt?'style="color:var(--muted)"':''}>${fmt(rv.amt)}</div>
<div class="vat-note">${rv.note}</div>
</div>`;
});
h+=`</div>`;
const pay=netto>=0;
h+=`<div class="vat-total-row">
<div>
<div class="vat-total-lbl">Ruta 49 — Moms att ${pay?'betala':'återfå'}</div>
<div style="font-size:.75rem;opacity:.65;margin-top:2px">${q} ${FISCAL} · Beräknat från kontobalanser</div>
</div>
<div class="vat-total-amt">${pay?'+':''}${fmt(Math.abs(netto))}</div>
</div>`;
h+=`<div style="font-size:.75rem;color:var(--muted);margin-top:4px">⚠️ Indikativt underlag. Verifiera mot bokförd data och kontakta KPMG (Andreas Vretblom) innan inlämning.</div>`;
body.innerHTML=h;
}catch(err){body.innerHTML=`<div style="color:var(--red);padding:20px">⚠️ Fel: ${err.message}</div>`;}
}
function exportVAT(){
const q=document.getElementById('vat-q-sel').value;
const a=document.createElement('a');
a.href=URL.createObjectURL(new Blob([`Momsunderlag LandveX AB\n${q} ${FISCAL}\nKontakta KPMG: andreas.vretblom@kpmg.se`],{type:'text/plain;charset=utf-8'}));
a.download=`moms-${q}-${FISCAL}.txt`;a.click();
}
// ═══════════════════════════════════════════════════════════════════════
// PERIODSTÄNGNING
// ═══════════════════════════════════════════════════════════════════════
// ─────────────────────────────────────────────────────────────
async function loadPeriod(){
const period=S.period||curPeriod();
document.getElementById('pc-label').textContent=pLabel(period);
let tbData=S.tb,status='open';
try{
const[pR,tbR]=await Promise.all([
api(`/api/ledger/periods/${period}/status`),
api(`/api/ledger/trial-balance?fiscal_year=${FISCAL}&period=${period}`),
]);
tbData=tbR.accounts||[];
status=pR.period?.status||'open';
}catch{}
const badge=document.getElementById('pc-badge');
const lbl={open:'Öppen',review:'Under granskning',approved:'Godkänd',closed:'Stängd'};
badge.className=`pbadge ${status}`;badge.textContent=`${lbl[status]||status}`;
// Workflow steps
const steps=['open','review','approved','closed'];
const idx=steps.indexOf(status);
steps.forEach((s,i)=>{
const el=document.getElementById('wf-'+s);
el.className='wf-step'+(i<idx?' done':i===idx?' active':'');
});
// Checklist live
const getTB=num=>{const a=tbData.find(x=>x.account_number===num);return a?parseFloat(a.balance||0):0;};
const td=tbData.reduce((s,a)=>s+fdeb(a),0);
const tc=tbData.reduce((s,a)=>s+fcrd(a),0);
const balOK=tbData.length>0&&Math.abs(td-tc)<1;
const obsOK=Math.abs(getTB('1689'))<1;
const vatOK=Math.abs(getTB('2611'))>0||tbData.length===0;
const draftN=S.journal.filter(e=>e.status==='draft').length;
const dftOK=draftN===0;
function setChk(ciId,cvId,ok,val){
const ic=document.getElementById(ciId);ic.className=`chk-icon ${ok?'ok':'warn'}`;ic.textContent=ok?'✅':'⚠️';
document.getElementById(cvId).textContent=val;
}
setChk('ci-bal','cv-bal',balOK,balOK?'✓ Balanserar':`Diff: ${fmt(Math.abs(td-tc))}`);
setChk('ci-obs','cv-obs',obsOK,obsOK?'✓ 0 kr':`${fmt(getTB('1689'))} kvar`);
setChk('ci-vat','cv-vat',vatOK,vatOK?`${fmt(Math.abs(getTB('2611')))}`:'Kontrollera');
setChk('ci-dft','cv-dft',dftOK,dftOK?'✓ Inga utkast':`${draftN} utkast`);
const allOK=balOK&&obsOK&&dftOK;
const am={open:{a:'submit-review',lbl:'📤 Skicka för granskning'},review:{a:'approve',lbl:'✅ Godkänn'},approved:{a:'close',lbl:'🔒 Stäng period'},closed:{a:'reopen',lbl:'🔓 Öppna igen'}};
const act=am[status];
const div=document.getElementById('pc-actions');div.innerHTML='';
if(act){
const dis=act.a==='close'&&!allOK?'disabled':'';
div.innerHTML=`<button class="btn-top" ${dis} onclick="doPeriodAct('${act.a}','${period}')">${act.lbl}</button>`;
if(act.a==='close'&&!allOK) div.innerHTML+=`<span style="font-size:.78rem;color:var(--red);margin-left:8px">⚠️ Åtgärda checklist</span>`;
}
if(tbData.length>0){
div.innerHTML+=`<button class="btn-outline" onclick="showSnapshot()" style="margin-left:4px">📸 Trial balance</button>`;
}
// Spara tbData för snapshot
window._pcTB=tbData;
}
function showSnapshot(){
const tb=window._pcTB||[];
if(!tb.length){openInfo('Trial Balance','Ingen data');return;}
let h=`<table class="det-table"><thead><tr><th>Konto</th><th>Namn</th><th style="text-align:right">Debet</th><th style="text-align:right">Kredit</th><th style="text-align:right">Saldo</th></tr></thead><tbody>`;
tb.forEach(a=>{
const b=fbal(a);
h+=`<tr><td style="font-family:monospace;font-size:.78rem">${a.account_number}</td><td>${esc(acname(a))}</td><td style="text-align:right;font-variant-numeric:tabular-nums">${fmt(fdeb(a))}</td><td style="text-align:right;font-variant-numeric:tabular-nums">${fmt(fcrd(a))}</td><td style="text-align:right;font-weight:700;font-variant-numeric:tabular-nums">${fmt(b)}</td></tr>`;
});
const td=tb.reduce((s,a)=>s+fdeb(a),0);
const tc=tb.reduce((s,a)=>s+fcrd(a),0);
h+=`<tr class="det-total"><td colspan="2">SUMMA</td><td style="text-align:right">${fmt(td)}</td><td style="text-align:right">${fmt(tc)}</td><td style="text-align:right;${Math.abs(td-tc)<1?'color:var(--green)':'color:var(--red)'}">${fmt(td-tc)}</td></tr></tbody></table>`;
openInfo(`Trial Balance — ${pLabel(S.period)}`,h);
}
async function doPeriodAct(action,period){
const btn=document.querySelector('#pc-actions button');
if(btn){btn.disabled=true;btn.textContent='⏳…';}
const fb=document.getElementById('pc-fb');
try{
const r=await api(`/api/ledger/periods/${period}/${action}`,'POST',{fiscal_year:FISCAL});
fb.innerHTML=`<span style="color:var(--green)">✅ ${r.message||'Uppdaterat'}</span>`;
await loadAll();loadPeriod();
}catch(e){fb.innerHTML=`<span style="color:var(--red)">❌ ${e.message}</span>`;}
if(btn)btn.disabled=false;
}
// ═══════════════════════════════════════════════════════════════════════
// NY VERIFIKATION
// ═══════════════════════════════════════════════════════════════════════
let lineN=0,allAccounts=[];
function openNewEntry(){
// Säkra att transaktions-fliken är aktiv först
if(!document.getElementById('tab-transactions').classList.contains('active')){
switchTab('transactions');
}
document.getElementById('entry-body').classList.add('open');
document.getElementById('entry-chev').style.transform='rotate(180deg)';
if(!lineN){addLine();addLine();}
// Scrolla efter att fliken hunnit bli synlig
requestAnimationFrame(()=>{
requestAnimationFrame(()=>{
document.getElementById('new-entry-card').scrollIntoView({behavior:'smooth',block:'start'});
});
});
}
function toggleEntry(){
document.getElementById('entry-body').classList.toggle('open');
const open=document.getElementById('entry-body').classList.contains('open');
document.getElementById('entry-chev').style.transform=open?'rotate(180deg)':'none';
}
function resetEntry(){
document.getElementById('e-date').value=today();
document.getElementById('e-desc').value='';document.getElementById('e-ref').value='';
document.getElementById('e-lines').innerHTML='';lineN=0;addLine();addLine();updateBal();
const fb=document.getElementById('e-fb');fb.className='feedback';fb.textContent='';
}
function addLine(){
const id=++lineN;
const div=document.createElement('div');div.className='e-line';div.id=`ln-${id}`;
div.innerHTML=`<div class="acc-wrap">
<input type="text" class="acc-in" id="ac-${id}" placeholder="Sök konto…" autocomplete="off"
oninput="filterAcc(${id})" onfocus="showAcc(${id})" onblur="hideAcc(${id})">
<div class="acc-drop" id="dr-${id}"></div>
</div>
<input type="number" class="num-in" id="db-${id}" placeholder="0" min="0" step="0.01" oninput="updateBal()">
<input type="number" class="num-in" id="cr-${id}" placeholder="0" min="0" step="0.01" oninput="updateBal()">
<button class="rm-btn" onclick="rmLine(${id})">✕</button>`;
document.getElementById('e-lines').appendChild(div);updateBal();
}
function rmLine(id){document.getElementById(`ln-${id}`)?.remove();updateBal();}
function filterAcc(id){
const q=document.getElementById(`ac-${id}`).value.toLowerCase();
const dr=document.getElementById(`dr-${id}`);
const matches=allAccounts.filter(a=>a.account_number.includes(q)||(acname(a)).toLowerCase().includes(q)).slice(0,20);
dr.innerHTML=matches.length
?matches.map(a=>`<div class="acc-opt" onmousedown="pickAcc(${id},'${a.account_number}','${esc(acname(a)).replace(/'/g,"\\'")}')"><span class="acc-code">${a.account_number}</span><span>${esc(acname(a))}</span></div>`).join('')
:`<div class="acc-opt" style="color:var(--muted);cursor:default">Inga konton</div>`;
dr.classList.add('open');
}
function showAcc(id){filterAcc(id);}
function hideAcc(id){setTimeout(()=>document.getElementById(`dr-${id}`)?.classList.remove('open'),180);}
function pickAcc(id,code,name){
const inp=document.getElementById(`ac-${id}`);
inp.value=`${code} ${name}`;inp.dataset.num=code;
document.getElementById(`dr-${id}`)?.classList.remove('open');
}
function updateBal(){
let d=0,c=0;
document.querySelectorAll('.e-line').forEach(ln=>{
const id=ln.id.replace('ln-','');
d+=parseFloat(document.getElementById(`db-${id}`)?.value||0)||0;
c+=parseFloat(document.getElementById(`cr-${id}`)?.value||0)||0;
});
const ind=document.getElementById('bal-ind');
document.getElementById('bal-d').textContent=fmt(d);
document.getElementById('bal-k').textContent=fmt(c);
const ok=d>0&&Math.abs(d-c)<0.01;
ind.className='bal-ind'+(ok?' ok':(d||c)?' bad':'');
document.getElementById('bal-txt').textContent=ok?'✅ Balanserad':(d||c)?`⚠️ Diff: ${fmt(Math.abs(d-c))}`:'Ange rader';
}
async function saveEntry(){
const date=document.getElementById('e-date').value;
const desc=document.getElementById('e-desc').value.trim();
const ref =document.getElementById('e-ref').value.trim();
const fb=document.getElementById('e-fb');
if(!date||!desc){fb.textContent='⚠️ Ange datum och beskrivning';fb.className='feedback err';return;}
const lines=[];let dbt=0,crt=0;
document.querySelectorAll('.e-line').forEach(ln=>{
const id=ln.id.replace('ln-','');
const inp=document.getElementById(`ac-${id}`);
const num=inp?.dataset.num||inp?.value.split(' ')[0];
const d=parseFloat(document.getElementById(`db-${id}`)?.value||0)||0;
const c=parseFloat(document.getElementById(`cr-${id}`)?.value||0)||0;
if(num&&(d||c)){lines.push({account_number:num,debit:d,credit:c});dbt+=d;crt+=c;}
});
if(lines.length<2){fb.textContent='⚠️ Minst 2 rader';fb.className='feedback err';return;}
if(Math.abs(dbt-crt)>0.01){fb.textContent=`⚠️ Diff: ${fmt(Math.abs(dbt-crt))}`;fb.className='feedback err';return;}
const btn=document.getElementById('save-btn');btn.disabled=true;btn.textContent='⏳ Sparar…';fb.className='feedback';
try{
const r=await api('/api/ledger/journal','POST',{date,description:desc,reference:ref||undefined,lines,fiscal_year:FISCAL,period:S.period});
const eid=r.id||r.entry_id||'?';
try{await api(`/api/ledger/journal/${eid}/post`,'POST',{});}catch{}
fb.textContent=`✅ Verifikat #${eid.toString().slice(0,8)} sparat`;fb.className='feedback ok';
resetEntry();await loadAll();
}catch(e){fb.textContent=`${e.message}`;fb.className='feedback err';}
btn.disabled=false;btn.textContent='💾 Spara';
}
// ═══════════════════════════════════════════════════════════════════════
// NY FAKTURA
// ═══════════════════════════════════════════════════════════════════════
function openNewInvoice(){
document.getElementById('inv-date').value=today();
const due=new Date();due.setDate(due.getDate()+30);
document.getElementById('inv-due').value=due.toISOString().slice(0,10);
['inv-cust','inv-amt','inv-ref','inv-desc'].forEach(id=>document.getElementById(id).value='');
document.getElementById('inv-vat').value='25';
document.getElementById('inv-fb').className='feedback';
document.getElementById('inv-preview').style.display='none';
openModal('modal-inv');
}
function previewInv(){
const amt=parseFloat(document.getElementById('inv-amt').value)||0;
const rate=parseInt(document.getElementById('inv-vat').value)||25;
const prev=document.getElementById('inv-preview');
if(amt>0){prev.style.display='';prev.innerHTML=`${fmt(amt)} + ${fmt(amt*rate/100)} moms (${rate}%) = <strong>${fmt(amt+amt*rate/100)}</strong> totalt`;}
else prev.style.display='none';
}
async function saveInvoice(){
const cust=document.getElementById('inv-cust').value.trim();
const date=document.getElementById('inv-date').value;
const due =document.getElementById('inv-due').value;
const amt =parseFloat(document.getElementById('inv-amt').value)||0;
const rate=parseInt(document.getElementById('inv-vat').value)||25;
const ref =document.getElementById('inv-ref').value.trim();
const desc=document.getElementById('inv-desc').value.trim();
const fb=document.getElementById('inv-fb');
if(!cust){fb.textContent='⚠️ Ange kund';fb.className='feedback err';return;}
if(amt<=0){fb.textContent='⚠️ Ange belopp';fb.className='feedback err';return;}
const vat=parseFloat((amt*rate/100).toFixed(2));
const tot=amt+vat;
const num=`FAK-${FISCAL}-${String((S.invoices?.length||0)+1).padStart(3,'0')}`;
const va=rate===25?'2611':rate===12?'2615':rate===6?'2616':null;
const ra=rate===25?'3010':rate===12?'3020':'3000';
const lines=[
{account_number:'1510',debit:tot,credit:0},
{account_number:ra,debit:0,credit:amt},
...(vat>0&&va?[{account_number:va,debit:0,credit:vat}]:[]),
];
const btn=document.getElementById('inv-save');btn.disabled=true;btn.textContent='⏳…';
try{
const r=await api('/api/ledger/journal','POST',{date,description:`${num} ${desc||cust}`,reference:num,lines,fiscal_year:FISCAL,period:S.period});
const eid=r.id||r.entry_id;
if(eid){try{await api(`/api/ledger/journal/${eid}/post`,'POST',{});}catch{}}
fb.textContent=`${num} skapad`;fb.className='feedback ok';
await loadInvoices();
setTimeout(()=>closeModal('modal-inv'),1500);
}catch(e){fb.textContent=`${e.message}`;fb.className='feedback err';}
btn.disabled=false;btn.textContent='💾 Spara & bokför';
}
// ═══════════════════════════════════════════════════════════════════════
// SIE4
// ═══════════════════════════════════════════════════════════════════════
function doSIE(){
fetch(`/api/ledger/export/sie4?fiscal_year=${FISCAL}`,{headers:{'x-internal-source':'ouroboros-system','x-tenant-id':TENANT}})
.then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.blob();})
.then(b=>{const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=`landvex-${FISCAL}.se`;a.click();})
.catch(e=>openInfo('SIE4 Export',`<div style="color:var(--red)">⚠️ Export misslyckades: ${e.message}</div>`));
}
// ═══════════════════════════════════════════════════════════════════════
// MODAL
// ═══════════════════════════════════════════════════════════════════════
function openModal(id){document.getElementById(id)?.classList.remove('hidden');}
function closeModal(id){document.getElementById(id)?.classList.add('hidden');}
function openInfo(title,html){
document.getElementById('mi-title').textContent=title;
document.getElementById('mi-body').innerHTML=html;
openModal('modal-info');
}
// ═══════════════════════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════════
// PROGNOS
// ═══════════════════════════════════════════════════════════════════════
async function loadForecast(){
// Hämta YTD trial balance
let tb = S.tb;
if(!tb.length){
try{ const r=await api('/api/ledger/trial-balance?fiscal_year=2026'); tb=r.accounts||[]; }catch{}
}
const get = (num,field) => { const a=tb.find(x=>x.account_number===num); return a?Math.abs(parseFloat(a[field]||0)):0; };
const getbal = num => { const a=tb.find(x=>x.account_number===num); return a?parseFloat(a.balance||0):0; };
// === NULÄGE ===
const kassa = tb.filter(a=>/^19/.test(a.account_number)).reduce((s,a)=>s+parseFloat(a.balance||0),0);
// Skulder: 2611 (moms ut), 2650 (momsredovisning), 2710 (personalskatt)
const skuld2611 = Math.max(0, -getbal('2611')); // kredit = skuld
const skuld2650 = Math.max(0, -getbal('2650'));
const skuld2710 = Math.max(0, -getbal('2710'));
const skulder = skuld2611 + skuld2650 + skuld2710;
const kasseEfter = kassa - skulder;
// Burn rate: genomsnitt kostnader per månad (6 månader)
const totKostnader = tb.filter(a=>/^[4-8]/.test(a.account_number)).reduce((s,a)=>s+Math.abs(parseFloat(a.balance||0)),0);
const burnPerMan = totKostnader / 6;
// Bekräftad intäkt per månad: Trygg Bil kontrakt = 222916 kr/mån
const TRYGGBIL_MAN = 222916;
// Runway i månader
const netto_man = TRYGGBIL_MAN - burnPerMan;
let runway;
if(netto_man > 0) runway = 'Positiv kassaflöde';
else runway = Math.max(0, Math.floor(kasseEfter / Math.abs(netto_man))) + ' mån runway';
// KPI-kort
document.getElementById('fc-kassa-v').textContent = fmt(kassa);
document.getElementById('fc-skulder-v').textContent = fmt(skulder);
const nettoEl = document.getElementById('fc-netto-v');
nettoEl.textContent = fmt(kasseEfter);
nettoEl.className = 'kpi-val ' + (kasseEfter > 50000 ? 'green' : kasseEfter > 0 ? 'amber' : 'red');
document.getElementById('fc-netto-card').className = 'kpi ' + (kasseEfter > 50000 ? 'green' : kasseEfter > 0 ? 'amber' : 'red');
document.getElementById('fc-burn-v').textContent = fmt(burnPerMan) + '/mån';
document.getElementById('fc-runway').textContent = runway;
// Alert om kritiskt läge
const alertEl = document.getElementById('fc-alert');
if(kasseEfter < 50000){
alertEl.style.display='';
alertEl.style.background='rgba(229,62,62,.08)';
alertEl.style.border='1.5px solid rgba(229,62,62,.3)';
alertEl.style.color='var(--red)';
alertEl.innerHTML = '🚨 <strong>KRITISKT:</strong> Kassa efter förfallna skulder är under 50 000 kr. Omedelbar åtgärd krävs.';
} else if(kasseEfter < 200000){
alertEl.style.display='';
alertEl.style.background='rgba(200,118,58,.08)';
alertEl.style.border='1.5px solid rgba(200,118,58,.3)';
alertEl.style.color='var(--amber)';
alertEl.innerHTML = '⚠️ <strong>VARNING:</strong> Låg likviditet. Betala skulder och säkra nästa intäkt.';
} else {
alertEl.style.display='none';
}
// === PROGNOS 6 månader ===
const now = new Date();
const curY = now.getFullYear(), curM = now.getMonth(); // 0-indexed
const MN_SV = ['Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'];
let kassaLopande = kasseEfter; // börja från kassa efter skulder
const rows = [];
// Månad 0 = denna månad (delvis), resterande = framtida
for(let i=1; i<=6; i++){
const mIdx = (curM + i) % 12;
const yr = curY + Math.floor((curM + i) / 12);
const lbl = MN_SV[mIdx] + ' ' + yr;
// Inbetalningar: Trygg Bil (222 916/mån) + marginal för övrigt (10%)
const inn = TRYGGBIL_MAN * 1.0;
// Utbetalningar: burn + extra för moms Q kvartalsslut
let ut = burnPerMan;
// Lägg på moms vid kvartalsskiften (apr, jul, okt, jan)
if([0,3,6,9].includes(mIdx)) ut += 110000; // ungefär moms var 3:e mån
const netto = inn - ut;
kassaLopande += netto;
rows.push({ lbl, inn, ut, netto, kassa: kassaLopande });
}
// Rita stapeldiagram
const maxAbs = Math.max(...rows.map(r=>Math.abs(r.kassa)), 1);
const chartEl = document.getElementById('fc-barchart');
const lblEl = document.getElementById('fc-barlabels');
chartEl.innerHTML=''; lblEl.innerHTML='';
rows.forEach(r=>{
const pct = Math.min(100, Math.abs(r.kassa) / maxAbs * 100);
const cls = r.kassa > 100000 ? 'pos' : r.kassa > 0 ? 'warn' : 'neg';
chartEl.innerHTML += `<div class="fc-bar-col"><div class="fc-bar ${cls}" style="height:${Math.max(3,pct*1.5)}px"></div><div class="fc-bv">${Math.round(r.kassa/1000)}k</div></div>`;
lblEl.innerHTML += `<div style="flex:1;text-align:center;font-size:.68rem">${r.lbl}</div>`;
});
// Tabell
const tbody = document.getElementById('fc-table-body');
tbody.innerHTML = rows.map((r,i)=>{
const kassaCls = r.kassa>100000?'color:var(--green)':r.kassa>0?'color:var(--amber)':'color:var(--red)';
return `<tr style="border-bottom:1px solid var(--border)${i===0?';background:var(--amber-bg)':''}">
<td style="padding:6px 8px;font-weight:${i===0?'700':'400'}">${r.lbl}${i===0?' ←':''} </td>
<td style="padding:6px 8px;text-align:right;color:var(--green);font-variant-numeric:tabular-nums">${fmt(r.inn)}</td>
<td style="padding:6px 8px;text-align:right;color:var(--red);font-variant-numeric:tabular-nums">${fmt(r.ut)}</td>
<td style="padding:6px 8px;text-align:right;font-weight:600;font-variant-numeric:tabular-nums;${r.netto>=0?'color:var(--green)':'color:var(--red)'}">${r.netto>=0?'+':''}${fmt(r.netto)}</td>
<td style="padding:6px 8px;text-align:right;font-weight:700;font-variant-numeric:tabular-nums;${kassaCls}">${fmt(r.kassa)}</td>
</tr>`;
}).join('');
// === KRITISKA ÅTGÄRDER ===
const actions = [];
if(skulder > 0) actions.push({dot:'r', text:`Betala förfallna skulder: <strong>${fmt(skulder)}</strong> (moms Q1/Q2 + personalskatt). Undvik dröjsmålsränta och skattetillägg.`});
if(kasseEfter < 100000) actions.push({dot:'r', text:`Kritisk likviditet: <strong>${fmt(kasseEfter)}</strong> kvar efter skulder. Kontakta Trygg Bil för förskottsbetalning eller dragning på kredit.`});
actions.push({dot:'a', text:`IT-kostnader <strong>${fmt(get('6230','balance'))}</strong> YTD (Lovable + AI). Störst enskild kostnad. Granska prenumerationer.`});
actions.push({dot:'a', text:`Resekostnader <strong>${fmt(get('5800','balance') + get('5801','balance'))}</strong> YTD. Sätt tak på 50 000 kr/kvartal.`});
if(get('1610','balance') > 0) actions.push({dot:'r', text:`Fordran anställd (1610): <strong>${fmt(get('1610','balance'))}</strong> — Winston överbetald lön. Driva in eller avskriv.`});
actions.push({dot:'g', text:`Trygg Bil-kontrakt bekräftat t.o.m. mars 2027. Säkrad intäkt 222 916 kr/mån = <strong>2 675 000 kr/år</strong>.`});
document.getElementById('fc-actions').innerHTML = actions.map(a=>
`<div class="act-row"><div class="act-dot ${a.dot}"></div><div>${a.text}</div></div>`
).join('');
// === BUDGETREKOMMENDATIONER ===
const recs = [
{cls:'crit', text:'<strong>Betala moms omgående.</strong> 332 826 kr (2650) är förfallen. Varje dag kostar 1,25% ränta + potentiellt skattetillägg.'},
{cls:'crit', text:'<strong>Sänk IT-budgeten.</strong> 450k YTD på Lovable = 75k/mån. Budgettak: 40k/mån. Spara 35k/mån = 420k/år.'},
{cls:'warn', text:'<strong>Resebudget.</strong> 288k YTD resor = 48k/mån. Sätt tak 30k/mån, kräv förhandsgodkännande >5k.'},
{cls:'warn', text:'<strong>Representation.</strong> 80k YTD ej avdragsgill (6072). Noll skattefördel. Sätt nollbudget tills lönsamhet är stabil.'},
{cls:'info', text:'<strong>Nästa intäkt.</strong> En ny kund à 100k/mån ger positiv kassatäckning med marginal. Prioritera Fas 1 quiXzoom-kunder.'},
{cls:'info', text:'<strong>USD-kassa 126k kr</strong> på Revolut USD. Konvertera till SEK för att täcka momsskuld om Nordea inte räcker.'},
];
document.getElementById('fc-recs').innerHTML = recs.map(r=>`<div class="rec ${r.cls}">${r.text}</div>`).join('');
// === INTÄKTSPOTENTIAL ===
document.getElementById('fc-revenue').innerHTML = `
<div class="rec info"><strong>Bekräftat:</strong> Trygg Bil 222 916 kr/mån (t.o.m. mars 2027)</div>
<div class="rec info"><strong>Pipeline:</strong> quiXzoom Fas 1 (aug 2026). Mål: 35 SME-kunder à 3080k/mån</div>
<div class="rec warn"><strong>Break-even:</strong> Behöver ~${fmt(burnPerMan)} kr/mån. Trygg Bil täcker ${Math.round(TRYGGBIL_MAN/burnPerMan*100)}% av burn.</div>
`;
// === KOSTNADSNEDBRYTNING ===
const costsRaw = tb.filter(a=>/^[4-8]/.test(a.account_number)&&Math.abs(parseFloat(a.balance||0))>1000)
.map(a=>({name:acname(a),num:a.account_number,ytd:Math.abs(parseFloat(a.balance||0)),snitt:Math.abs(parseFloat(a.balance||0))/6}))
.sort((a,b)=>b.snitt-a.snitt);
const maxSnitt = costsRaw[0]?.snitt || 1;
document.getElementById('fc-total-burn').textContent = fmt(burnPerMan) + '/mån genomsnitt';
document.getElementById('fc-costbars').innerHTML = costsRaw.slice(0,12).map(c=>{
const pct = Math.min(100, c.snitt/maxSnitt*100);
const cls = pct>60?'high':pct>30?'med':'low';
return `<div class="cbar">
<div class="cbar-lbl" title="${esc(c.name)}">${esc(c.num)} ${esc(c.name)}</div>
<div class="cbar-track"><div class="cbar-fill ${cls}" style="width:${pct}%"></div></div>
<div class="cbar-amt">${fmt(c.snitt)}/mån</div>
</div>`;
}).join('');
}
// ═══════════════════════════════════════════════════════════════════════
// KVITTON
// ═══════════════════════════════════════════════════════════════════════
let _rcptEntryId = null;
async function openReceiptModal(entryId, entryDesc, entryNr) {
_rcptEntryId = entryId;
document.getElementById('rcpt-title').textContent = `📎 Kvitton — #${entryNr}`;
document.getElementById('rcpt-entry-info').textContent = entryDesc || '';
document.getElementById('rcpt-fb').textContent = '';
document.getElementById('rcpt-upload-list').innerHTML = '';
// Hämta befintliga kvitton
const existEl = document.getElementById('rcpt-existing');
existEl.innerHTML = '<div style="color:var(--muted);font-size:.8rem">Laddar…</div>';
openModal('modal-receipt');
try {
const r = await api(`/api/ledger/journal/${entryId}/receipts`);
renderExistingReceipts(r.receipts || []);
} catch(e) {
existEl.innerHTML = '';
}
// Drag & drop
const dz = document.getElementById('rcpt-dropzone');
dz.ondragover = e => { e.preventDefault(); dz.classList.add('drag'); };
dz.ondragleave = () => dz.classList.remove('drag');
dz.ondrop = e => { e.preventDefault(); dz.classList.remove('drag'); handleReceiptFiles(e.dataTransfer.files); };
}
function renderExistingReceipts(receipts) {
const el = document.getElementById('rcpt-existing');
if (!receipts.length) { el.innerHTML = '<div style="color:var(--muted);font-size:.8rem;margin-bottom:10px">Inga kvitton uppladdade ännu.</div>'; return; }
const icons = { 'image/jpeg':'🖼️','image/png':'🖼️','application/pdf':'📄','image/heic':'🖼️' };
el.innerHTML = `<div style="font-size:.75rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">${receipts.length} kvitto${receipts.length>1?'n':''} uppladdade</div>` +
receipts.map(r => {
const icon = icons[r.contentType] || '📎';
const kb = r.fileSize ? ` · ${Math.round(r.fileSize/1024)} KB` : '';
const by = r.uploaded_by && r.uploaded_by !== 'unknown' ? ` · ${r.uploaded_by}` : '';
const dt = r.uploaded_at ? ` · ${r.uploaded_at.slice(0,10)}` : '';
return `<div class="receipt-item">
<span class="receipt-item-icon">${icon}</span>
<span class="receipt-item-name" title="${esc(r.filename)}">${esc(r.filename)}</span>
<span class="receipt-item-size">${kb}${by}${dt}</span>
<a href="${esc(r.url)}" target="_blank" class="receipt-item-link">Öppna →</a>
</div>`;
}).join('') + '<div style="height:1px;background:var(--border);margin:10px 0"></div>';
}
async function handleReceiptFiles(files) {
if (!_rcptEntryId) return;
const listEl = document.getElementById('rcpt-upload-list');
const fbEl = document.getElementById('rcpt-fb');
fbEl.textContent = '';
for (const file of files) {
if (file.size > 10 * 1024 * 1024) {
fbEl.innerHTML = `<span style="color:var(--red)">⚠️ ${esc(file.name)} är för stor (max 10 MB)</span>`;
continue;
}
const itemId = 'up_' + Date.now() + '_' + Math.random().toString(36).slice(2);
listEl.innerHTML += `<div id="${itemId}" class="receipt-item">
<span class="receipt-item-icon">⏳</span>
<span class="receipt-item-name">${esc(file.name)}</span>
<span class="receipt-item-size">${Math.round(file.size/1024)} KB</span>
<span style="color:var(--muted);font-size:.75rem" id="${itemId}-st">Hämtar URL…</span>
</div><div class="upload-progress" id="${itemId}-bar"><div class="upload-progress-fill" style="width:5%"></div></div>`;
try {
// POST fil direkt till API (raw bytes)
document.querySelector(`#${itemId}-st`).textContent = 'Laddar upp…';
document.querySelector(`#${itemId}-bar .upload-progress-fill`).style.width = '20%';
const ctrl2 = new AbortController();
const uploadRes = await fetch(`/api/ledger/journal/${_rcptEntryId}/upload`, {
method: 'POST',
headers: {
'Content-Type': file.type || 'application/octet-stream',
'X-Filename': file.name,
'x-internal-source': 'ouroboros-system',
'x-tenant-id': TENANT,
},
body: file,
signal: ctrl2.signal,
});
document.querySelector(`#${itemId}-bar .upload-progress-fill`).style.width = '90%';
if (!uploadRes.ok) throw new Error('Upload misslyckades: ' + uploadRes.status);
const confRes = await uploadRes.json();
if (!confRes.ok) throw new Error(confRes.error || 'Okänt fel');
document.querySelector(`#${itemId}-bar .upload-progress-fill`).style.width = '100%';
document.querySelector(`#${itemId}`).querySelector('.receipt-item-icon').textContent = '✅';
document.querySelector(`#${itemId}-st`).innerHTML = `<a href="${esc(confRes.url||'')}" target="_blank" class="receipt-item-link">Öppna →</a>`;
renderExistingReceipts(confRes.receipts);
} catch(err) {
document.querySelector(`#${itemId}`)?.querySelector('.receipt-item-icon') && (document.querySelector(`#${itemId}`).querySelector('.receipt-item-icon').textContent = '❌');
document.querySelector(`#${itemId}-st`) && (document.querySelector(`#${itemId}-st`).textContent = err.message);
fbEl.innerHTML = `<span style="color:var(--red)">❌ ${esc(err.message)}</span>`;
}
}
}
function init(){
S.token=TOKEN;S.period=curPeriod();S.periodMode='month';
document.getElementById('e-date').value=today();
buildPeriodSelects();
loadAll();
setInterval(loadAll,60000);
}
document.addEventListener('DOMContentLoaded',init);
// ═══════════════════════════════════════════════════════════
// SKATT & MOMS — SKV-skulder + kalender
// ═══════════════════════════════════════════════════════════
function loadVATExtras() {
const tb = S.tb;
if (!tb || !tb.length) return;
const getbal = nr => { const a = tb.find(x => x.account_number === nr); return a ? parseFloat(a.balance||0) : 0; };
const fmt2 = n => Math.round(Math.abs(n)).toLocaleString('sv-SE') + ' kr';
// SKV-skulder
const skulder = [
{ nr:'2650', namn:'Momsdeklaration (Q1+Q2)', inst:'Betala till Skatteverket', sev:'critical' },
{ nr:'2710', namn:'Personalskatt', inst:'Betala senast 12:e varje månad', sev:'critical' },
{ nr:'2731', namn:'Arbetsgivaravgift (AGA)', inst:'Betalas med personalskatt', sev:'critical' },
{ nr:'2611', namn:'Utgående moms ej omförd', inst:'Omför till 2650 vid kvartalets slut', sev:'high' },
{ nr:'2615', namn:'Moms omvänd skattskyldighet', inst:'Matchar konto 2645 — kvittar ut', sev:'info' },
];
let totalSkuld = 0;
let bodyHtml = '<table style="width:100%;border-collapse:collapse">';
bodyHtml += '<thead><tr style="border-bottom:1.5px solid #F2F2F7"><th style="padding:8px 14px;text-align:left;font-size:.7rem;color:#8E8E93;text-transform:uppercase">Konto</th><th style="padding:8px 14px;text-align:left;font-size:.7rem;color:#8E8E93;text-transform:uppercase">Skuld</th><th style="padding:8px 14px;text-align:right;font-size:.7rem;color:#8E8E93;text-transform:uppercase">Belopp</th></tr></thead><tbody>';
for (const s of skulder) {
const bal = getbal(s.nr);
const skuld = Math.max(0, -bal);
if (skuld < 1 && s.sev !== 'info') continue;
const col = s.sev==='critical'?'#FF3B30':s.sev==='high'?'#FF9500':'#8E8E93';
const sym = s.sev==='critical'?'⛔':s.sev==='high'?'⚠️':'️';
const amtStr = skuld > 0 ? `<strong style="color:${col}">${fmt2(skuld)}</strong>` : '<span style="color:#34C759">✅ 0</span>';
bodyHtml += `<tr style="border-bottom:1px solid #F2F2F7">
<td style="padding:10px 14px;font-size:.78rem;color:#666">${sym} ${s.nr}</td>
<td style="padding:10px 14px"><div style="font-size:.82rem;font-weight:600;color:#1c1c1e">${s.namn}</div><div style="font-size:.72rem;color:#8E8E93;margin-top:2px">${s.inst}</div></td>
<td style="padding:10px 14px;text-align:right">${amtStr}</td>
</tr>`;
if (s.sev !== 'info') totalSkuld += skuld;
}
// Totalt
bodyHtml += `<tr style="border-top:2px solid #1c1c1e;background:#FAFAFA">
<td colspan="2" style="padding:10px 14px;font-size:.83rem;font-weight:700">Totalt att betala till SKV nu</td>
<td style="padding:10px 14px;text-align:right;font-size:.9rem;font-weight:800;color:${totalSkuld>0?'#FF3B30':'#34C759'}">${totalSkuld>0?fmt2(totalSkuld):'✅ Betalt'}</td>
</tr>`;
bodyHtml += '</tbody></table>';
const skvBody = document.getElementById('skv-skulder-body');
if (skvBody) skvBody.innerHTML = bodyHtml;
const badge = document.getElementById('skv-total-badge');
if (badge) badge.textContent = totalSkuld > 0 ? fmt2(totalSkuld) : '✅ OK';
// Skattekalender
const now = new Date();
const m = now.getMonth()+1, y = now.getFullYear();
const cal = [
{ datum:`${y}-${String(m).padStart(2,'0')}-12`, namn:'Personalskatt + AGA', konto:'2710 / 2731', sev: getbal('2710') < -100 || getbal('2731') < -100 ? 'critical' : 'ok' },
{ datum:`${y}-05-26`, namn:'Momsdeklaration Q1', konto:'2650', sev: getbal('2650') < -100 ? 'critical' : 'ok' },
{ datum:`${y}-08-26`, namn:'Momsdeklaration Q2', konto:'2650', sev:'upcoming' },
{ datum:`${y}-11-26`, namn:'Momsdeklaration Q3', konto:'2650', sev:'upcoming' },
].sort((a,b)=>a.datum.localeCompare(b.datum));
let calHtml = '';
for (const e of cal) {
const daysLeft = Math.ceil((new Date(e.datum)-now)/(1000*60*60*24));
const col = e.sev==='critical'?'#FF3B30':e.sev==='ok'?'#34C759':'#FF9500';
const lbl = daysLeft < 0 ? '🔴 FÖRFALLEN' : daysLeft===0 ? '🔴 IDAG' : daysLeft<=7 ? `⚠️ om ${daysLeft} dagar` : `${daysLeft} dagar kvar`;
calHtml += `<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #F2F2F7">
<div><div style="font-size:.83rem;font-weight:600">${e.namn}</div><div style="font-size:.73rem;color:#8E8E93">Konto ${e.konto} — förfaller ${e.datum}</div></div>
<span style="font-size:.76rem;font-weight:700;color:${col};white-space:nowrap">${lbl}</span>
</div>`;
}
const calBody = document.getElementById('skv-calendar-body');
if (calBody) calBody.innerHTML = calHtml || '<div style="color:#34C759">✅ Inga kommande förfallodatum</div>';
}
// ═══════════════════════════════════════════════════════════
// LÖNER — loadPayroll()
// ═══════════════════════════════════════════════════════════
async function loadPayroll() {
const fmtN = n => Math.round(Math.abs(n)).toLocaleString('sv-SE') + ' kr';
const fmtC = (n, pos='#34C759', neg='#FF3B30') => `<span style="color:${n>=0?pos:neg};font-weight:700">${fmtN(n)}</span>`;
// Hämta direkt från API med posted-filter — aldrig trial balance (kan ha stale data)
let entries = [];
try {
// Hämta alla lönerelaterade verifikat (status=posted)
const r = await api('/api/ledger/journal?limit=500');
entries = (r.entries || []).filter(e => e.status === 'posted');
} catch(e) {
document.getElementById('pr-persons-body').innerHTML = '<div style="padding:14px;color:#FF3B30">Kunde inte ladda lönedata: ' + e.message + '</div>';
return;
}
// Aggregera per konto — BARA posted
const loneKonton = {'7010':0,'7210':0,'7510':0};
const perioder = {};
const personPoster = {'7010':[],'7210':[]};
for (const e of entries) {
if (!e.lines) continue;
const hasLon = e.lines.some(l => ['7010','7210','7510'].includes(l.account_number));
if (!hasLon) continue;
for (const l of e.lines) {
const acc = l.account_number;
if (loneKonton[acc] !== undefined) {
loneKonton[acc] += parseFloat(l.debit||0) - parseFloat(l.credit||0);
}
if (personPoster[acc]) {
personPoster[acc].push({ entry: e.entry_number, date: e.entry_date, desc: e.description, debit: parseFloat(l.debit||0), credit: parseFloat(l.credit||0) });
}
}
// Period-aggregering
const p = e.period;
if (!perioder[p]) perioder[p] = {lon:0, aga:0, entries:[]};
for (const l of e.lines) {
if (['7010','7210'].includes(l.account_number)) perioder[p].lon += parseFloat(l.debit||0) - parseFloat(l.credit||0);
if (l.account_number==='7510') perioder[p].aga += parseFloat(l.debit||0);
}
perioder[p].entries.push(e.entry_number);
}
// KPI — använd trial balance för skatteskulder (de är rätt)
const tb = S.tb;
const getbal = nr => { const a = tb.find(x => x.account_number === nr); return a ? parseFloat(a.balance||0) : 0; };
const bruttoLon = Math.max(0, loneKonton['7010'] + loneKonton['7210']);
const aga = Math.max(0, loneKonton['7510']);
const persSkSkuld = Math.max(0, -getbal('2710'));
const agaSkuld = Math.max(0, -getbal('2731'));
const skuldSKV = persSkSkuld + agaSkuld;
document.getElementById('pr-lon-v').textContent = fmtN(bruttoLon);
document.getElementById('pr-aga-v').textContent = fmtN(aga);
document.getElementById('pr-skuld-v').textContent = fmtN(skuldSKV);
// Räkna aktiva — baserat på konton med saldo > 0
const antalPersoner = (loneKonton['7210'] > 0 ? 1 : 0) + (loneKonton['7010'] > 0 ? 1 : 0);
document.getElementById('pr-antal-v').textContent = antalPersoner > 0 ? antalPersoner + ' pers.' : '—';
// ── LÖNER PER PERSON — klickbara rader ──────────────────────────────
const persons = [
{ namn:'Erik Svensson', konto:'7210', titel:'VD', not:'Konto 7210 — förskott utan lönespecifikation', lon: Math.max(0, loneKonton['7210']) },
{ namn:'Anställda', konto:'7010', titel:'Kollektivanställda', not:'Konto 7010 — Johan Berglund m.fl.', lon: Math.max(0, loneKonton['7010']) },
].filter(p => p.lon > 0);
let persHtml = '<table style="width:100%;border-collapse:collapse">';
persHtml += '<thead><tr style="border-bottom:1.5px solid #F2F2F7">'
+ '<th style="padding:8px 14px;text-align:left;font-size:.7rem;color:#8E8E93;text-transform:uppercase">Person</th>'
+ '<th style="padding:8px 14px;text-align:right;font-size:.7rem;color:#8E8E93;text-transform:uppercase">Bruttolön YTD</th>'
+ '</tr></thead><tbody>';
for (const p of persons) {
const poster = personPoster[p.konto];
window._payrollPersons = window._payrollPersons || {};
window._payrollPersons[p.konto] = { namn: p.namn, not: p.not, poster };
persHtml += `<tr style="border-bottom:1px solid #F2F2F7;cursor:pointer"
onclick="openPayrollPerson('${p.konto}')"
onmouseover="this.style.background='#F9F9F9'" onmouseout="this.style.background=''">
<td style="padding:10px 14px">
<div style="font-size:.84rem;font-weight:600">${p.namn}</div>
<div style="font-size:.72rem;color:#8E8E93">${p.titel} — konto ${p.konto}</div>
<div style="font-size:.71rem;color:#007AFF;margin-top:2px">${p.not} — klicka för poster </div>
</td>
<td style="padding:10px 14px;text-align:right;font-size:.88rem;font-weight:700">${fmtN(p.lon)}</td>
</tr>`;
}
if (!persons.length) persHtml += '<tr><td colspan="2" style="padding:16px 14px;color:#8E8E93;font-size:.82rem">Inga löneposter bokförda</td></tr>';
persHtml += `<tr style="border-top:2px solid #1c1c1e;background:#FAFAFA">
<td style="padding:9px 14px;font-weight:700;font-size:.83rem">Totalt bruttolöner YTD</td>
<td style="padding:9px 14px;text-align:right;font-size:.9rem;font-weight:800">${fmtN(bruttoLon)}</td>
</tr></tbody></table>`;
document.getElementById('pr-persons-body').innerHTML = persHtml;
// ── LÖNER PER PERIOD — klickbara rader ──────────────────────────────
const sortedPeriods = Object.keys(perioder).sort();
let perHtml = '<table style="width:100%;border-collapse:collapse">';
perHtml += '<thead><tr style="border-bottom:1.5px solid #F2F2F7">'
+ '<th style="padding:8px 14px;text-align:left;font-size:.7rem;color:#8E8E93;text-transform:uppercase">Period</th>'
+ '<th style="padding:8px 14px;text-align:right;font-size:.7rem;color:#8E8E93;text-transform:uppercase">Bruttolön</th>'
+ '<th style="padding:8px 14px;text-align:right;font-size:.7rem;color:#8E8E93;text-transform:uppercase">AGA</th>'
+ '<th style="padding:8px 14px;text-align:right;font-size:.7rem;color:#8E8E93;text-transform:uppercase">Total</th>'
+ '</tr></thead><tbody>';
let grandLon = 0, grandAga = 0;
for (const p of sortedPeriods) {
const d = perioder[p];
const lon = Math.max(0, d.lon), ag = Math.max(0, d.aga);
grandLon += lon; grandAga += ag;
const verNr = d.entries.join(', #');
perHtml += `<tr style="border-bottom:1px solid #F2F2F7;cursor:pointer"
onclick="openInfo('Löner ${p}','<div style=\\'font-size:.82rem\\'><b>Verifikat:</b> #${verNr}</div><div style=\\'margin-top:8px;font-size:.82rem\\'>Bruttolön: <b>${fmtN(lon)}</b><br>AGA: <b>${fmtN(ag)}</b><br>Total: <b>${fmtN(lon+ag)}</b></div>')"
onmouseover="this.style.background='#F9F9F9'" onmouseout="this.style.background=''">
<td style="padding:9px 14px;font-size:.82rem;font-weight:600">${p} <span style="font-size:.7rem;color:#007AFF"></span></td>
<td style="padding:9px 14px;text-align:right;font-size:.82rem">${fmtN(lon)}</td>
<td style="padding:9px 14px;text-align:right;font-size:.82rem">${fmtN(ag)}</td>
<td style="padding:9px 14px;text-align:right;font-size:.84rem;font-weight:700">${fmtN(lon+ag)}</td>
</tr>`;
}
if (!sortedPeriods.length) {
perHtml += '<tr><td colspan="4" style="padding:16px 14px;color:#8E8E93;font-size:.82rem">Inga löneposter hittade</td></tr>';
} else {
perHtml += `<tr style="border-top:2px solid #1c1c1e;background:#FAFAFA">
<td style="padding:9px 14px;font-weight:700;font-size:.83rem">Totalt YTD</td>
<td style="padding:9px 14px;text-align:right;font-size:.83rem;font-weight:700">${fmtN(grandLon)}</td>
<td style="padding:9px 14px;text-align:right;font-size:.83rem;font-weight:700">${fmtN(grandAga)}</td>
<td style="padding:9px 14px;text-align:right;font-size:.9rem;font-weight:800">${fmtN(grandLon+grandAga)}</td>
</tr>`;
}
perHtml += '</tbody></table>';
document.getElementById('pr-periods-body').innerHTML = perHtml;
// ── AGA & PERSONALSKATT ──────────────────────────────────────────────
const skvData = [
{ namn:'Bokförd AGA YTD (konto 7510)', val: aga, not:'31,42% × bruttolön', col:'#FF9500' },
{ namn:'Personalskatt att betala (konto 2710)', val: persSkSkuld, not:'Betalas till SKV senast 12:e', col: persSkSkuld>0?'#FF3B30':'#34C759' },
{ namn:'Arbetsgivaravgift att betala (konto 2731)', val: agaSkuld, not:'Betalas med personalskatt', col: agaSkuld>0?'#FF3B30':'#34C759' },
];
let skvHtml = '';
for (const r of skvData) {
skvHtml += `<div style="display:flex;justify-content:space-between;align-items:center;padding:9px 0;border-bottom:1px solid #F2F2F7;cursor:pointer"
onclick="showSkuldModal()"
onmouseover="this.style.background='#F9F9F9'" onmouseout="this.style.background=''">
<div><div style="font-size:.83rem;font-weight:600">${r.namn}</div>
<div style="font-size:.72rem;color:#8E8E93">${r.not} <span style="color:#007AFF">— klicka för detaljer </span></div></div>
<span style="font-size:.9rem;font-weight:800;color:${r.col};white-space:nowrap;margin-left:12px">${fmtN(r.val)}</span>
</div>`;
}
const totalToPay = persSkSkuld + agaSkuld;
skvHtml += `<div style="display:flex;justify-content:space-between;align-items:baseline;padding:10px 0;border-top:2px solid #1c1c1e;margin-top:2px">
<span style="font-size:.85rem;font-weight:700">Totalt att betala till SKV</span>
<span style="font-size:.95rem;font-weight:800;color:${totalToPay>0?'#FF3B30':'#34C759'}">${totalToPay>0?fmtN(totalToPay):'✅ Betalt'}</span>
</div>`;
const badge = document.getElementById('pr-skv-badge');
if (badge) { badge.textContent = totalToPay>0 ? fmtN(totalToPay) + ' att betala' : '✅'; badge.style.background = totalToPay>0?'#FF3B30':'#34C759'; badge.style.color='#fff'; }
document.getElementById('pr-skv-body').innerHTML = skvHtml;
}
function openPayrollPerson(konto) {
const data = window._payrollPersons && window._payrollPersons[konto];
if (!data) return;
const fmtN2 = n => Math.round(Math.abs(n)).toLocaleString('sv-SE') + ' kr';
const rows = data.poster.map(e =>
'<div style="padding:7px 0;border-bottom:1px solid #F2F2F7;display:flex;justify-content:space-between">' +
'<div><div style="font-size:.82rem;font-weight:600">Ver. #' + e.entry + '</div>' +
'<div style="font-size:.72rem;color:#8E8E93">' + (e.date||'').slice(0,10) + ' — ' + (e.desc||'').slice(0,50) + '</div></div>' +
'<span style="font-size:.85rem;font-weight:700;color:#1c1c1e">' + fmtN2(e.debit - e.credit) + '</span></div>'
).join('');
const html = '<div style="font-size:.78rem;color:#8E8E93;margin-bottom:10px">' + data.not + '</div>' + rows;
openInfo(data.namn + ' — löneposter', html);
}
// ═══════════════════════════════════════════════════════════
// ONBOARDING — rollbaserad, kontextmedveten
// ═══════════════════════════════════════════════════════════
const OB_PROFILES = {
ceo: {
name: 'Erik Svensson', greeting: 'Välkommen tillbaka, Erik', color: '#007AFF',
steps: [
{ icon: '📊', title: 'Din kontrollpanel', text: 'Kassa, intäkter och resultat i realtid. Klicka på valfri siffra för detaljer — konton, skulder, valutauppdelning.', target: '.kpi-row' },
{ icon: '⛔', title: 'Tasks — det som kräver din handling', text: 'Tasks-fliken visar allt som väntar: obetalda skatter, öppna perioder, fordringar. Rött = kritiskt. Börja alltid här.', target: '#tasks-tab-btn' },
{ icon: '🏦', title: 'Skatt & Moms — en flik, allt samlat', text: 'Moms, personalskatt och arbetsgivaravgift på ett ställe. Förfallodatum och exakta belopp. Inga överraskningar.', target: '[onclick="switchTab(\'vat\')"]' },
{ icon: '👥', title: 'Löner — per person och period', text: 'Se bruttolöner, AGA och vad som är kvar att betala till SKV. Klicka "Att betala SKV " för full uppdelning.', target: '[onclick="switchTab(\'payroll\')"]' },
{ icon: '📷', title: 'Utlägg — foto räcker', text: 'Öppna ett verifikat i Transaktioner → 📷 Ta foto. Kvittot kopplas direkt. Fungerar från mobilen.', target: '[onclick="switchTab(\'transactions\')"]' },
{ icon: '📤', title: 'SIE4 till revisorn', text: 'Exportera bokföringen med ett klick. Skickas automatiskt till Andreas Vretblom (KPMG) vid periodsslut.', target: '[onclick="doSIE()"]' }
]
},
revisor: {
name: 'Andreas Vretblom', greeting: 'Välkommen, Andreas', color: '#5856D6',
steps: [
{ icon: '📋', title: 'Bokföringsöversikt LandveX AB', text: 'Trial balance, P&L och balansräkning. FY 2025/2026 (majapril). Välj period i period-baren ovan.', target: '.period-bar' },
{ icon: '⚖️', title: 'Balansen håller', text: 'Dubbel bokföring verifierad: 9 718 214 kr debet = kredit. 5 perioder stängda (janmaj 2026).', target: '.kpi-row' },
{ icon: '📤', title: 'SIE4-export', text: 'Klicka SIE4 för att ladda ned hela bokföringen. Direkt import i era revisionssystem.', target: '[onclick="doSIE()"]' },
{ icon: '🏦', title: 'Skatteskulder', text: 'Momsdeklaration 332 826 kr obetald. Personalskatt 26 212 kr. Detaljer i Skatt & Moms-fliken.', target: '[onclick="switchTab(\'vat\')"]' },
{ icon: '📋', title: 'Verifikat & underlag', text: '1 092 bokförda verifikat. Klicka på en rad för kvitton och underlag.', target: '[onclick="switchTab(\'transactions\')"]' }
]
},
cto: {
name: 'Johan Berglund', greeting: 'Välkommen, Johan', color: '#FF9500',
steps: [
{ icon: '⚡', title: 'Systemstatus & API', text: 'Bokförings-API:t körs på aamos-ledger (port 3250). Realtidsdata. Kontrollera att alla endpoints svarar i Tasks-fliken.', target: '#tasks-tab-btn' },
{ icon: '📊', title: 'Kassaflöde & burn', text: 'Kassa och burn rate i realtid. Klicka KPI-korten för underliggande kontodata och verifikat.', target: '.kpi-row' },
{ icon: '🔁', title: 'Löpande bokföring', text: 'Transaktioner och verifikat. Kontrollera att API-integrationer (Stripe, Swan) bokför korrekt.', target: '[onclick="switchTab(\'transactions\')"]' },
{ icon: '💰', title: 'Löner & kostnader', text: 'Löneutbetalningar, AGA och prelskatt. Dina löneposter bokförs på konto 7010.', target: '[onclick="switchTab(\'payroll\')"]' },
{ icon: '📤', title: 'SIE4-export', text: 'Exportera bokföringen för revisor (KPMG). Används vid periodsslut och på begäran.', target: '[onclick="doSIE()"]' }
]
},
bookkeeper: {
name: 'Ekonomiassistent', greeting: 'Välkommen', color: '#34C759',
steps: [
{ icon: '', title: 'Bokför en transaktion', text: 'Klicka " Ny verifikation". Fyll i datum, beskrivning och debet/kredit-rader. Systemet kontrollerar att det balanserar.', target: '.btn-top' },
{ icon: '📋', title: 'Transaktionslistan', text: 'Alla verifikat med sök på leverantör, belopp eller konto. Klicka för detaljer och kvitton.', target: '[onclick="switchTab(\'transactions\')"]' },
{ icon: '📷', title: 'Ladda upp kvitto', text: 'Öppna ett verifikat → dra fil hit eller 📷 Ta foto. JPG, PNG, PDF upp till 12 MB.', target: null },
{ icon: '✅', title: 'Tasks — din att-göra-lista', text: 'Visa vad som saknar underlag eller behöver åtgärd. Arbeta igenom uppifrån.', target: '#tasks-tab-btn' },
{ icon: '📅', title: 'Periodstängning', text: 'Period-fliken → Submit for review när månaden är klar. Controllern godkänner.', target: '[onclick="switchTab(\'period\')"]' }
]
},
default: {
name: '', greeting: 'Välkommen till LandveX Ekonomi', color: '#007AFF',
steps: [
{ icon: '📊', title: 'Kassa & resultat', text: 'Klicka på valfri siffra för detaljer.', target: '.kpi-row' },
{ icon: '✅', title: 'Tasks', text: 'Tasks-fliken visar vad som kräver åtgärd.', target: '#tasks-tab-btn' },
{ icon: '', title: 'Bokför', text: '" Ny verifikation" för att bokföra.', target: '.btn-top' }
]
}
};
function detectRole() {
const urlRole = new URLSearchParams(location.search).get('role');
if (urlRole && OB_PROFILES[urlRole]) return urlRole;
const stored = localStorage.getItem('ob-role');
if (stored && OB_PROFILES[stored]) return stored;
return 'ceo';
}
let obStep = 0;
let obActive = false;
let obProfile = null;
function obShowRolePicker() {
const roles = [
{ key: 'ceo', icon: '👔', label: 'Erik Svensson', sub: 'CEO / Ägare' },
{ key: 'cto', icon: '⚡', label: 'Johan Berglund', sub: 'CTO' },
{ key: 'revisor', icon: '🏛', label: 'Andreas Vretblom', sub: 'Revisor (KPMG)' },
{ key: 'bookkeeper', icon: '📋', label: 'Ekonomiassistent', sub: 'Bokförare' },
];
const html = '<div style="font-size:.78rem;color:#8E8E93;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px">Välj din roll för en anpassad guide</div>' +
roles.map(r =>
'<div onclick="closeModal(\'modal-info\');localStorage.setItem(\'ob-role\',\'' + r.key + '\');obStart(\'' + r.key + '\')" ' +
'style="display:flex;align-items:center;gap:12px;padding:13px;border-radius:10px;cursor:pointer;border:1.5px solid #F2F2F7;margin-bottom:8px" ' +
'onmouseover="this.style.background=\'#F2F2F7\'" onmouseout="this.style.background=\'\'">' +
'<span style="font-size:1.5rem">' + r.icon + '</span>' +
'<div><div style="font-size:.87rem;font-weight:600">' + r.label + '</div>' +
'<div style="font-size:.75rem;color:#8E8E93">' + r.sub + '</div></div></div>'
).join('') +
'<div style="margin-top:6px;font-size:.74rem;color:#8E8E93;text-align:center">Välj en gång — sparas automatiskt. Byt via ? uppe till höger.</div>';
openInfo('Välkommen — vem är du?', html);
}
function obStart(roleOverride) {
if (document.getElementById('ob-backdrop')) return;
const role = roleOverride || detectRole();
obProfile = OB_PROFILES[role] || OB_PROFILES.default;
localStorage.setItem('ob-seen', '1');
localStorage.setItem('ob-role', role);
obStep = 0; obActive = true;
const bd = document.createElement('div');
bd.id = 'ob-backdrop';
bd.innerHTML =
'<div id="ob-highlight" style="display:none"></div>' +
'<div id="ob-box" style="--ob-accent:' + obProfile.color + '">' +
'<button id="ob-close" onclick="obEnd()" title="Stäng">✕</button>' +
'<div id="ob-step-num"></div><div id="ob-icon"></div>' +
'<div id="ob-title"></div><div id="ob-text"></div>' +
'<div id="ob-actions"><div id="ob-dots"></div>' +
'<div style="display:flex;gap:8px;align-items:center">' +
'<button id="ob-skip" onclick="obEnd()">Hoppa över</button>' +
'<button id="ob-next" onclick="obNext()">Nästa →</button>' +
'</div></div></div>';
document.body.appendChild(bd);
bd.addEventListener('click', e => { if (e.target === bd) obEnd(); });
obRender();
}
function obRender() {
if (!obProfile) return;
const steps = obProfile.steps;
const s = steps[obStep];
document.getElementById('ob-step-num').textContent =
obStep === 0 && obProfile.greeting ? obProfile.greeting : 'Steg ' + (obStep+1) + ' av ' + steps.length;
document.getElementById('ob-icon').textContent = s.icon;
document.getElementById('ob-title').textContent = s.title;
document.getElementById('ob-text').textContent = s.text;
document.getElementById('ob-next').textContent = obStep === steps.length-1 ? '✓ Klar' : 'Nästa →';
document.getElementById('ob-skip').style.display = obStep === steps.length-1 ? 'none' : '';
document.getElementById('ob-dots').innerHTML = steps.map((_,i) =>
'<span class="ob-dot' + (i===obStep?' active':'') + '" style="' + (i===obStep?'background:'+obProfile.color:'') + '"></span>'
).join('');
const hl = document.getElementById('ob-highlight');
if (s.target) {
const el = document.querySelector(s.target);
if (el) {
const r = el.getBoundingClientRect();
hl.style.cssText = 'display:block;top:' + (r.top+scrollY-8) + 'px;left:' + (r.left-8) + 'px;width:' + (r.width+16) + 'px;height:' + (r.height+16) + 'px;border:2.5px solid ' + obProfile.color + ';border-radius:10px;position:absolute;pointer-events:none;z-index:9998;box-shadow:0 0 0 4000px rgba(0,0,0,.45)';
} else { hl.style.display='none'; }
} else { hl.style.display='none'; }
}
function obNext() {
if (!obProfile || obStep >= obProfile.steps.length-1) { obEnd(); return; }
obStep++; obRender();
}
function obEnd() {
const bd = document.getElementById('ob-backdrop');
if (bd) bd.remove();
obActive = false;
}
window.addEventListener('DOMContentLoaded', () => {
if (!localStorage.getItem('ob-seen')) { setTimeout(obShowRolePicker, 1200); }
});
</script>
</body>
</html>