e11f70b347
* refactor: optimize page loading and data fetching * fix: resolve recurring production runtime errors * feat: add MCP company and customer updates * fix: handle year-end tax adjustments * feat: harden annual report compliance * fix: expand invoice logo and font support * fix: sanitize API route error responses * fix: sanitize user-facing error messages * feat: persist onboarding and tax assessment notices * fix: reduce cloud backup audit churn * feat: refine invoice editor layout * fix: show saved tax adjustments in INK2 * fix: complete annual report API mappings * docs: record operational safeguards and decisions * fix: harden annual report review findings * fix: adjust column span for description based on VAT registration * New css class name
306 lines
25 KiB
HTML
306 lines
25 KiB
HTML
<!doctype html>
|
||
<html lang="sv">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>PROTOTYP — Kundfaktura redesign (round 9: back to clean editorial, ALL fields)</title>
|
||
<!--
|
||
THROWAWAY PROTOTYPE — /grilling-frontend-prototyping.
|
||
ROUND 9 — back to the clean editorial language of rounds 2-3 (which Emil
|
||
liked), now carrying EVERY field. No new borders, no nested boxes, no chips,
|
||
no emoji, no ⋮ clutter. Advanced per-line bits (ROT/RUT, periodisering, konto)
|
||
render as a quiet indented line with one thin rule. Three early favourites,
|
||
all complete: Faktabladet, Rubrik, Stegduk. Opens on Fullt to verify nothing
|
||
is missing.
|
||
-->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Hedvig+Letters+Serif:opsz@12..24&family=Inter:wght@400;450;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#ffffff; --fg:#1c1b19; --muted:#6f6b64; --faint:#a19c92;
|
||
--secondary-60:#f5f1e9; --border:#e3ded4; --border-strong:#cfc9bd;
|
||
--primary:#1c1b19; --primary-fg:#faf9f6;
|
||
--sans:'Inter',system-ui,-apple-system,Segoe UI,sans-serif; --serif:'Hedvig Letters Serif',Georgia,serif;
|
||
}
|
||
*{box-sizing:border-box} html,body{margin:0;height:100%}
|
||
body{background:#fff;color:var(--fg);font-family:var(--sans);font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}
|
||
.tnum{font-variant-numeric:tabular-nums} .serif{font-family:var(--serif);font-weight:400;letter-spacing:-0.012em}
|
||
.muted{color:var(--muted)} .faint{color:var(--faint)} button{font-family:inherit}
|
||
.app{display:grid;grid-template-columns:248px 1fr;height:100vh;overflow:hidden}
|
||
.side{border-right:1px solid var(--border);padding:22px 16px;display:flex;flex-direction:column;gap:2px;background:#fff}
|
||
.brand{font-family:var(--serif);font-size:20px;letter-spacing:-0.02em;padding:6px 10px 20px}
|
||
.nav a{display:flex;align-items:center;gap:11px;padding:8px 10px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:13.5px}
|
||
.nav a .dotnav{width:5px;height:5px;border-radius:50%;background:var(--border-strong)}
|
||
.nav a.active{background:#f1ede5;color:var(--fg)} .nav a.active .dotnav{background:var(--fg)}
|
||
.nav a:hover:not(.active){background:#f7f4ee} .side .spacer{flex:1}
|
||
.who{display:flex;align-items:center;gap:10px;padding:10px;border-top:1px solid var(--border);margin-top:8px}
|
||
.avatar{width:26px;height:26px;border-radius:50%;background:#eae5db;display:grid;place-items:center;font-size:11px;color:var(--muted)}
|
||
.main{overflow:auto;position:relative} .stage{min-height:100%;background:#faf8f4}
|
||
|
||
.inp,select.inp{width:100%;border:1px solid var(--border-strong);border-radius:8px;background:#fff;height:40px;padding:0 12px;font-family:inherit;font-size:14px;color:var(--fg)}
|
||
select.inp{appearance:none;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a19c92' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");background-repeat:no-repeat;background-position:right 11px center;padding-right:30px;cursor:pointer}
|
||
.inp:focus,select.inp:focus{outline:0;border-color:var(--fg);box-shadow:0 0 0 3px rgba(28,27,25,.08)}
|
||
.inp::placeholder{color:var(--faint)} textarea.inp{height:auto;min-height:56px;padding:10px 12px;resize:vertical;line-height:1.55}
|
||
.lbl{display:block;font-size:12px;color:var(--muted);margin-bottom:6px} .req{color:#b4533f;margin-left:2px}
|
||
.btn{display:inline-flex;align-items:center;justify-content:center;height:44px;padding:0 22px;border-radius:9px;border:0;background:var(--primary);color:var(--primary-fg);font-size:14px;font-weight:500;cursor:pointer} .btn:hover{background:#322e26}
|
||
.obtn{display:inline-flex;align-items:center;justify-content:center;height:44px;padding:0 18px;border-radius:9px;border:1px solid var(--border-strong);background:#fff;color:var(--fg);font-size:14px;cursor:pointer} .obtn:hover{background:#f7f4ee}
|
||
.tbtn{background:0;border:0;color:var(--muted);font-size:13.5px;cursor:pointer;padding:8px 0} .tbtn:hover{color:var(--fg)}
|
||
.doclabel{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
|
||
|
||
.surface{margin:0 auto;background:#fff;border:1px solid var(--border);border-radius:14px}
|
||
.wrap{padding:40px 24px 60px}
|
||
.sech{font-family:var(--serif);font-size:16px}
|
||
.metagrid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px 22px}
|
||
.tabs{display:flex;gap:22px} .tabs button{background:0;border:0;padding:5px 0;font-size:14px;color:var(--muted);cursor:pointer;border-bottom:1.5px solid transparent} .tabs button.on{color:var(--fg);border-bottom-color:var(--fg)}
|
||
|
||
/* clean line list (borderless cells, hairline rows) */
|
||
.lg{width:100%;border-collapse:collapse}
|
||
.lg th{font-weight:400;color:var(--faint);font-size:11px;text-align:left;padding:0 12px 10px 0}
|
||
.lg thead tr{border-bottom:1px solid var(--border)} .lg th.r,.lg td.r{text-align:right}
|
||
.lg td{padding:3px 12px 3px 0;border:0;vertical-align:middle} .lg tbody tr.l{border-bottom:1px solid var(--border)}
|
||
.lg .c{border:0;background:transparent;width:100%;padding:11px 0;font-family:inherit;font-size:14px;color:var(--fg)}
|
||
.lg .c.r{text-align:right;font-variant-numeric:tabular-nums} .lg .c:focus{outline:0;box-shadow:0 -2px 0 var(--fg) inset}
|
||
.lg .amt{padding:11px 0;font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}
|
||
.lg .rm{opacity:0;color:var(--faint);cursor:pointer;font-size:15px} .lg tr.l:hover .rm{opacity:1}
|
||
.art{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:var(--faint);margin-right:8px}
|
||
/* quiet advanced line: one thin left rule, no box/chip/emoji */
|
||
.adv{margin:0 0 4px;padding:6px 0 12px 14px;border-left:2px solid var(--border);display:flex;flex-wrap:wrap;align-items:flex-end;gap:14px 20px}
|
||
.advk{font-size:12px;color:var(--muted);font-weight:500;align-self:center}
|
||
.micro{display:block;font-size:11px;color:var(--faint);margin-bottom:3px}
|
||
.ins,select.ins{border:1px solid var(--border-strong);border-radius:7px;height:34px;padding:0 10px;font-size:13px;background:#fff;font-family:inherit;color:var(--fg)}
|
||
select.ins{appearance:none;padding-right:24px;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='11' height='11' viewBox='0 0 24 24' fill='none' stroke='%23a19c92' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");background-repeat:no-repeat;background-position:right 8px center}
|
||
.advamt{font-size:12.5px;color:var(--muted);align-self:center} .advnote{width:100%;font-size:12px;color:var(--muted)}
|
||
.addrow{display:flex;gap:20px;margin-top:14px} .addrow .tbtn{padding-top:4px}
|
||
|
||
.sums{width:290px}
|
||
.sumrow{display:flex;justify-content:space-between;padding:5px 0;font-size:13.5px}
|
||
.sumtot{display:flex;justify-content:space-between;align-items:baseline;margin-top:10px;padding-top:14px;border-top:1px solid var(--border)} .sumtot .val{font-family:var(--serif);font-size:27px}
|
||
.sw{width:38px;height:22px;border-radius:999px;background:var(--border-strong);position:relative;cursor:pointer;flex:none} .sw.on{background:var(--fg)} .sw::after{content:'';position:absolute;width:18px;height:18px;border-radius:50%;background:#fff;top:2px;left:2px;transition:.15s} .sw.on::after{left:18px}
|
||
.step{padding:24px 34px;border-top:1px solid var(--border)} .stephead{display:flex;align-items:center;gap:10px;margin-bottom:16px}
|
||
.stepnum{width:22px;height:22px;border-radius:50%;background:var(--secondary-60);display:grid;place-items:center;font-size:12px;font-variant-numeric:tabular-nums}
|
||
|
||
.picker{position:fixed;right:22px;bottom:22px;width:344px;z-index:9999;background:#141311;color:#f4f0e8;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.34);font-size:13px;user-select:none}
|
||
.picker-h{display:flex;align-items:center;gap:8px;padding:10px 12px;cursor:grab;border-bottom:1px solid #2a2823} .picker-h .dot{width:8px;height:8px;border-radius:50%;background:#c9a24a} .picker-h b{font-weight:600}
|
||
.picker-b{padding:12px} .picker-nav{display:flex;align-items:center;gap:10px;margin-bottom:8px}
|
||
.picker-nav button{width:34px;height:34px;border-radius:8px;border:1px solid #35322b;background:#211f1b;color:#f4f0e8;font-size:16px;cursor:pointer;display:grid;place-items:center} .picker-nav button:hover{background:#2c2924}
|
||
.picker-name{flex:1;text-align:center} .picker-name .n{font-family:var(--serif);font-size:16px} .picker-name .k{font-size:11px;color:#a49d8d;letter-spacing:.08em}
|
||
.picker-desc{color:#b8b1a1;font-size:12px;line-height:1.45;min-height:60px}
|
||
.picker-states{display:flex;gap:6px;margin-top:10px;border-top:1px solid #2a2823;padding-top:10px}
|
||
.picker-states button{flex:1;height:30px;border-radius:7px;border:1px solid #35322b;background:#211f1b;color:#cdc7b8;cursor:pointer;font-size:12px}
|
||
.picker-states button.on{background:#f4f0e8;color:#141311;border-color:#f4f0e8;font-weight:600} .picker-count{font-size:11px;color:#8f887a;margin-left:auto}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<aside class="side">
|
||
<div class="brand">Accounted</div>
|
||
<nav class="nav">
|
||
<a href="#"><span class="dotnav"></span>Översikt</a>
|
||
<a href="#" class="active"><span class="dotnav"></span>Fakturor</a>
|
||
<a href="#"><span class="dotnav"></span>Kvitton</a>
|
||
<a href="#"><span class="dotnav"></span>Bokföring</a>
|
||
<a href="#"><span class="dotnav"></span>Rapporter</a>
|
||
<a href="#"><span class="dotnav"></span>Moms & skatt</a>
|
||
<a href="#"><span class="dotnav"></span>Inställningar</a>
|
||
</nav>
|
||
<div class="spacer"></div>
|
||
<div class="who"><div class="avatar">KS</div><div style="font-size:12.5px"><div>Kvist Studio AB</div><div class="faint" style="font-size:11px">emil@kviststudio.se</div></div></div>
|
||
</aside>
|
||
<main class="main"><div class="stage" id="stage"></div></main>
|
||
</div>
|
||
|
||
<div class="picker" id="picker">
|
||
<div class="picker-h" id="pickerHandle"><span class="dot"></span><b>Redesign</b> · rond 9 · ren + alla fält<span class="picker-count" id="pcount"></span></div>
|
||
<div class="picker-b">
|
||
<div class="picker-nav">
|
||
<button id="prev" aria-label="Föregående">‹</button>
|
||
<div class="picker-name"><div class="n" id="pname">–</div><div class="k" id="pkey"></div></div>
|
||
<button id="next" aria-label="Nästa">›</button>
|
||
</div>
|
||
<div class="picker-desc" id="pdesc"></div>
|
||
<div class="picker-states">
|
||
<button data-state="empty">Tom</button>
|
||
<button data-state="filled">Ifylld</button>
|
||
<button data-state="full" class="on">Fullt</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const money=(n,dec=false)=>(n).toLocaleString('sv-SE',dec?{minimumFractionDigits:2,maximumFractionDigits:2}:{})+' kr';
|
||
const amt=n=>Number.isInteger(n)?money(n):money(n,true);
|
||
const CUST={name:'Björklund Design AB',org:'556677-8899',addr:'Kungsgatan 12, 111 43 Stockholm',ref:'Anna Björklund'};
|
||
const SIMPLE=[{desc:'Webbutveckling, sprint 3',qty:24,unit:'tim',price:950,vat:25},{desc:'UX-rådgivning',qty:6,unit:'tim',price:1200,vat:25}];
|
||
const FULLROWS=[
|
||
{art:'A102',desc:'Webbutveckling, sprint 3',qty:24,unit:'tim',price:950,vat:25,rot:true},
|
||
{desc:'UX-rådgivning',qty:6,unit:'tim',price:1200,vat:25,accrual:true},
|
||
{desc:'Serverdrift, 12 mån',qty:1,unit:'st',price:2999.60,vat:25,konto:true},
|
||
{textRow:true,desc:'Allt arbete är utfört enligt avtal 2026-05-14.'},
|
||
];
|
||
const TT={empty:{sub:0,vat:0,pay:0},filled:{sub:30000,vat:7500,pay:37500},full:{sub:32999.60,vat:8249.90,pay:34410,rot:6840,round:0.50,total:41249.50}};
|
||
const V=s=>s!=='empty';
|
||
function pv(p){return p===''?'':(Number.isInteger(p)?p:p.toLocaleString('sv-SE',{minimumFractionDigits:2,maximumFractionDigits:2}));}
|
||
function rowsFor(s){return s==='empty'?[{desc:'',qty:'',unit:'st',price:'',vat:25}]:(s==='full'?FULLROWS:SIMPLE);}
|
||
|
||
function advClean(l){
|
||
if(l.rot) return `<tr><td colspan="7" style="padding:0"><div class="adv"><span class="advk">ROT-avdrag 30 %</span>
|
||
<div><span class="micro">Arbetstyp</span><select class="ins"><option>Bygg och renovering</option></select></div>
|
||
<div><span class="micro">Timmar</span><input class="ins tnum" style="width:90px" value="24"></div>
|
||
<span class="advamt tnum">−${money(6840,true)}</span>
|
||
<span class="advnote">Endast arbetskostnad ger skattereduktion, fakturera material på egen rad.</span></div></td></tr>`;
|
||
if(l.accrual) return `<tr><td colspan="7" style="padding:0"><div class="adv"><span class="advk">Periodisering</span>
|
||
<div><span class="micro">Från</span><input class="ins tnum" style="width:120px" value="2026-07-01"></div>
|
||
<div><span class="micro">Till</span><input class="ins tnum" style="width:120px" value="2026-12-31"></div>
|
||
<div><span class="micro">Balanskonto</span><select class="ins"><option>2979 Upplupna intäkter</option></select></div></div></td></tr>`;
|
||
if(l.konto) return `<tr><td colspan="7" style="padding:0"><div class="adv"><span class="advk">Försäljningskonto</span>
|
||
<div><select class="ins" style="min-width:240px"><option>3540 Fakturerade kostnader</option></select></div>
|
||
<span class="advnote" style="width:auto;align-self:center">ersätter det momssatshärledda kontot</span></div></td></tr>`;
|
||
return '';
|
||
}
|
||
function lineList(state){
|
||
const rows=rowsFor(state).map(l=>{
|
||
if(l.textRow) return `<tr class="l"><td colspan="6"><input class="c" style="color:var(--muted);font-style:italic" value="${l.desc}" placeholder="Fritext, notering på raden"></td><td class="r"><span class="rm">×</span></td></tr>`;
|
||
const has=l.qty!==''&&l.price!==''; const lt=has?l.qty*l.price:0;
|
||
return `<tr class="l">
|
||
<td><div style="display:flex;align-items:center">${l.art?`<span class="art">${l.art}</span>`:''}<input class="c" value="${l.desc}" placeholder="Beskrivning"></div></td>
|
||
<td style="width:60px" class="r"><input class="c r" value="${l.qty}" placeholder="1"></td>
|
||
<td style="width:56px"><input class="c" value="${l.unit}"></td>
|
||
<td style="width:96px" class="r"><input class="c r" value="${pv(l.price)}" placeholder="0"></td>
|
||
<td style="width:54px" class="r"><input class="c r" value="${l.vat}%"></td>
|
||
<td style="width:116px" class="r amt">${has?amt(lt):'—'}</td>
|
||
<td style="width:20px" class="r"><span class="rm">×</span></td>
|
||
</tr>${state==='full'?advClean(l):''}`;
|
||
}).join('');
|
||
const addAdv = state==='full'?'':'<button class="tbtn" style="color:var(--faint)">Skattereduktion / periodisering</button>';
|
||
return `<table class="lg"><thead><tr><th>Beskrivning</th><th class="r">Antal</th><th>Enhet</th><th class="r">À-pris</th><th class="r">Moms</th><th class="r">Belopp</th><th></th></tr></thead><tbody>${rows}</tbody></table>
|
||
<div class="addrow"><button class="tbtn">+ Lägg till rad</button><button class="tbtn">Välj artikel</button><button class="tbtn">Fritext</button>${addAdv}</div>`;
|
||
}
|
||
function metaGrid(state){const v=V(state);return `<div class="metagrid">
|
||
<div><span class="lbl">Fakturadatum<span class="req">*</span></span><input class="inp tnum" value="${v?'2026-07-21':''}" placeholder="åååå-mm-dd"></div>
|
||
<div><span class="lbl">Förfallodatum<span class="req">*</span></span><input class="inp tnum" value="${v?'2026-08-20':''}" placeholder="+30 dagar"></div>
|
||
<div><span class="lbl">Leveransdatum</span><input class="inp tnum" value="${state==='full'?'2026-07-18':''}" placeholder="valfritt"></div>
|
||
<div><span class="lbl">Valuta</span><select class="inp"><option>SEK</option><option>EUR</option><option>USD</option></select></div>
|
||
<div><span class="lbl">Er referens</span><input class="inp" value="${v?CUST.ref:''}" placeholder="kontaktperson"></div>
|
||
<div><span class="lbl">Vår referens</span><input class="inp" value="${v?'Emil Mattsson':''}" placeholder="du"></div>
|
||
<div><span class="lbl">Betallänk</span><input class="inp" value="${state==='full'?'buy.stripe.com/…':''}" placeholder="https://…"></div>
|
||
<div><span class="lbl">Kostnadsställe</span><input class="inp" value="${state==='full'?'Konsult':''}" placeholder="valfritt"></div>
|
||
<div><span class="lbl">Projekt</span><input class="inp" value="${state==='full'?'Webb 2026':''}" placeholder="valfritt"></div>
|
||
</div>`;}
|
||
function rotClaim(state){
|
||
if(state!=='full') return '';
|
||
return `<div style="margin-top:8px"><div class="sech" style="margin-bottom:14px">Skattereduktion <span class="muted" style="font-size:12.5px">· uppgifter för Skatteverket (ROT)</span></div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px 22px">
|
||
<div><span class="lbl">Personnummer (köpare)<span class="req">*</span></span><input class="inp" placeholder="ÅÅÅÅMMDD-XXXX"></div>
|
||
<div><span class="lbl">Fastighetsbeteckning<span class="req">*</span></span><input class="inp" value="Stockholm Väsby 1:23"></div>
|
||
</div></div>`;
|
||
}
|
||
function notes(state){return `<textarea class="inp" rows="2" placeholder="Valfri text som visas på fakturan">${state==='empty'?'':'Tack för förtroendet. Betalning sker via bankgiro 123-4567 senast på förfallodagen.'}</textarea>`;}
|
||
function summary(state){
|
||
const d=TT[state]; const m=n=>state==='full'?money(n,true):money(n);
|
||
return `<div class="sums">
|
||
<div class="sumrow"><span class="muted">Delsumma</span><span class="tnum">${m(d.sub)}</span></div>
|
||
<div class="sumrow"><span class="muted">Moms 25 %</span><span class="tnum">${m(d.vat)}</span></div>
|
||
${d.round?`<div class="sumrow"><span class="muted">Öresavrundning</span><span class="tnum">+${money(d.round,true)}</span></div>`:''}
|
||
${d.rot?`<div class="sumrow"><span class="muted">ROT-avdrag</span><span class="tnum">−${money(d.rot,true)}</span></div>`:''}
|
||
<div class="sumtot"><span>Att betala</span><span class="val tnum">${money(d.pay)}</span></div>
|
||
${d.rot?`<div class="sumrow" style="margin-top:6px"><span class="muted">Total inkl. moms</span><span class="tnum">${money(d.total,true)}</span></div>`:''}
|
||
<div class="sumrow" style="margin-top:14px;align-items:center"><span class="muted">Öresavrundning</span><span class="sw ${V(state)?'on':''}"></span></div>
|
||
</div>`;
|
||
}
|
||
const actions=`<div style="display:flex;gap:12px;align-items:center"><button class="btn">Granska & skapa</button><button class="obtn">Spara utkast</button><button class="tbtn">Förhandsgranska</button></div>`;
|
||
const custBlock=(state)=> state==='empty'
|
||
? `<input class="inp" style="height:44px;max-width:440px;font-size:15px" placeholder="Sök kund, eller skapa ny">`
|
||
: `<div style="display:flex;justify-content:space-between;align-items:flex-start"><div><div style="font-weight:500;font-size:15px">${CUST.name}</div><div class="muted" style="font-size:12.5px">${CUST.org} · ${CUST.addr}</div></div><button class="tbtn">Byt kund</button></div>`;
|
||
|
||
/* A — Faktabladet (letterhead) */
|
||
function vLetter(state){
|
||
return `<div class="wrap"><div class="surface" style="max-width:880px;padding:44px 48px 40px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:30px">
|
||
<div class="tabs"><button class="on">Faktura</button><button>Proforma</button><button>Följesedel</button></div>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:34px">
|
||
<div><div class="serif" style="font-size:21px">Kvist Studio AB</div><div class="muted" style="margin-top:4px;font-size:12.5px">Hornsgatan 1, 118 20 Stockholm · Org 559111-2233</div></div>
|
||
<div style="text-align:right"><div class="doclabel" style="letter-spacing:.22em">Faktura</div><div class="serif tnum" style="font-size:26px">F2026-014</div></div>
|
||
</div>
|
||
<div style="margin-bottom:26px"><div class="doclabel" style="margin-bottom:10px">Faktureras till</div>${custBlock(state)}</div>
|
||
<div style="margin-bottom:30px">${metaGrid(state)}</div>
|
||
<div class="sech" style="margin-bottom:12px">Rader</div>
|
||
${lineList(state)}
|
||
${state==='full'?`<div style="margin-top:26px">${rotClaim(state)}</div>`:''}
|
||
<div style="display:grid;grid-template-columns:1fr 300px;gap:36px;margin-top:32px">
|
||
<div><div class="sech" style="margin-bottom:12px">Anteckningar</div>${notes(state)}</div>
|
||
<div>${summary(state)}</div>
|
||
</div>
|
||
<hr style="border:0;border-top:1px solid var(--border);margin:28px 0 20px">${actions}
|
||
</div></div>`;
|
||
}
|
||
|
||
/* B — Rubrik (editorial title) */
|
||
function vTitle(state){
|
||
return `<div class="wrap"><div class="surface" style="max-width:880px;padding:0">
|
||
<div style="padding:30px 40px 24px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:flex-end">
|
||
<div><div class="doclabel" style="margin-bottom:8px">Faktura F2026-014</div>
|
||
${V(state)?`<div class="serif" style="font-size:28px">${CUST.name}</div><div class="muted" style="font-size:12.5px;margin-top:2px">${CUST.org} · ${CUST.addr}</div>`:`<div class="serif" style="font-size:28px;color:var(--faint)">Ny faktura</div>`}</div>
|
||
<div class="tabs" style="padding-bottom:4px"><button class="on">Faktura</button><button>Proforma</button><button>Följesedel</button></div>
|
||
</div>
|
||
${state==='empty'?`<div style="padding:20px 40px 0">${custBlock(state)}</div>`:''}
|
||
<div style="padding:24px 40px 0"><div class="doclabel" style="margin-bottom:14px">Detaljer</div>${metaGrid(state)}</div>
|
||
<div style="padding:24px 40px 0"><div class="sech" style="margin-bottom:12px">Rader</div>${lineList(state)}</div>
|
||
${state==='full'?`<div style="padding:26px 40px 0">${rotClaim(state)}</div>`:''}
|
||
<div style="padding:26px 40px 0"><div class="sech" style="margin-bottom:12px">Anteckningar</div>${notes(state)}</div>
|
||
<div style="padding:28px 40px 34px;margin-top:20px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:flex-end;gap:32px">
|
||
<div>${actions}</div><div>${summary(state)}</div>
|
||
</div>
|
||
</div></div>`;
|
||
}
|
||
|
||
/* C — Stegduk (numbered steps) */
|
||
function vSteps(state){
|
||
const step=(n,label,body,hint='')=>`<div class="step"><div class="stephead"><span class="stepnum">${n}</span><span class="sech">${label}</span>${hint?`<span class="muted" style="font-size:12px">${hint}</span>`:''}</div>${body}</div>`;
|
||
return `<div class="wrap"><div class="surface" style="max-width:820px;padding:0">
|
||
<div style="padding:24px 34px 8px;display:flex;justify-content:space-between;align-items:center"><div class="doclabel">Ny kundfaktura · F2026-014</div>
|
||
<div class="tabs"><button class="on">Faktura</button><button>Proforma</button><button>Följesedel</button></div></div>
|
||
${step(1,'Kund', custBlock(state))}
|
||
${step(2,'Rader', lineList(state))}
|
||
${step(3,'Villkor', metaGrid(state))}
|
||
${step(4,'Skattereduktion', state==='full'?rotClaim(state).replace('<div style="margin-top:8px">','<div>'):`<div class="muted" style="font-size:12.5px">Inga ROT/RUT-avdrag. Läggs till per rad under Rader.</div>`, state==='full'?'':'valfritt')}
|
||
${step(5,'Anteckningar', notes(state),'valfritt')}
|
||
<div class="step" style="display:flex;justify-content:space-between;align-items:flex-end;gap:32px"><div>${actions}</div><div>${summary(state)}</div></div>
|
||
</div></div>`;
|
||
}
|
||
|
||
const VARIANTS=[
|
||
{ key:'faktabladet', name:'Faktabladet', render:vLetter,
|
||
desc:'Fakturablad med brevhuvud: logga/org uppe, nr till höger, kund, sedan villkoren i ett luftigt fältrutnät, rader, summa. Alla fält, dokumentkänsla.' },
|
||
{ key:'rubrik', name:'Rubrik', render:vTitle,
|
||
desc:'Kundnamnet som stor serif-rubrik, all meta i en lugn "Detaljer"-yta, rena rader. Redaktionellt och lätt att läsa. Håller allt utan att kännas fullt.' },
|
||
{ key:'stegduk', name:'Stegduk', render:vSteps,
|
||
desc:'Numrerade steg: Kund → Rader → Villkor → Skattereduktion → Anteckningar → Summa. Varje fält får sin plats, inget trängs. Tydligast.' },
|
||
];
|
||
|
||
let idx=0, state='full';
|
||
const stage=document.getElementById('stage');
|
||
function draw(){
|
||
const v=VARIANTS[idx];
|
||
stage.innerHTML=v.render(state);
|
||
document.getElementById('pname').textContent=v.name;
|
||
document.getElementById('pkey').textContent=v.key.toUpperCase();
|
||
document.getElementById('pdesc').textContent=v.desc;
|
||
document.getElementById('pcount').textContent=` ${idx+1}/${VARIANTS.length}`;
|
||
stage.parentElement.scrollTop=0;
|
||
}
|
||
function go(d){ idx=(idx+d+VARIANTS.length)%VARIANTS.length; draw(); }
|
||
document.getElementById('prev').onclick=()=>go(-1);
|
||
document.getElementById('next').onclick=()=>go(1);
|
||
document.querySelectorAll('.picker-states button').forEach(b=>{ b.onclick=()=>{ state=b.dataset.state; document.querySelectorAll('.picker-states button').forEach(x=>x.classList.toggle('on',x===b)); draw(); }; });
|
||
document.addEventListener('keydown',e=>{ const t=e.target.tagName; if(t==='INPUT'||t==='TEXTAREA'||t==='SELECT'||e.target.isContentEditable) return; if(e.key==='ArrowLeft') go(-1); if(e.key==='ArrowRight') go(1); });
|
||
(function(){ const p=document.getElementById('picker'),h=document.getElementById('pickerHandle'); let dx=0,dy=0,drag=false;
|
||
h.addEventListener('mousedown',e=>{drag=true;dx=e.clientX-p.offsetLeft;dy=e.clientY-p.offsetTop;p.style.right='auto';p.style.bottom='auto';h.style.cursor='grabbing';});
|
||
document.addEventListener('mousemove',e=>{if(!drag)return;p.style.left=(e.clientX-dx)+'px';p.style.top=(e.clientY-dy)+'px';});
|
||
document.addEventListener('mouseup',()=>{drag=false;h.style.cursor='grab';}); })();
|
||
draw();
|
||
</script>
|
||
</body>
|
||
</html>
|