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
282 lines
23 KiB
HTML
282 lines
23 KiB
HTML
<!doctype html>
|
||
<html lang="sv">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>PROTOTYPE — R4 finish (F1–F4)</title>
|
||
<!--
|
||
THROWAWAY PROTOTYPE. Round 5 (leaf): base = R4 (split rows, borderless,
|
||
grouped economy) with a FULL aligned header incl. Beskrivning + Belopp.
|
||
Body + header locked; only the add-row affordance and row-actions vary (2x2).
|
||
Switch: draggable picker (bottom-right) or ← / → keys.
|
||
-->
|
||
<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=Geist:wght@400;450;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#f4f3ef;--sheet:#ffffff;--fg:#211f1b;--muted:#7c766d;--faint:#a6a096;
|
||
--border:#e7e3db;--border-strong:#d9d4ca;--field-border:#dcd7cd;
|
||
--beige:#e9e5dc;--beige-hover:#f0ede6;--beige-soft:#f4f1ea;
|
||
--success:#5f7355;--destructive:#b0503a;--r-sm:6px;
|
||
--serif:'Hedvig Letters Serif',Georgia,serif;--sans:'Geist','Inter',system-ui,sans-serif;
|
||
--ring:0 0 0 3px rgba(120,113,108,.16);
|
||
}
|
||
*{box-sizing:border-box}html,body{margin:0;padding:0}
|
||
body{background:var(--bg);color:var(--fg);font-family:var(--sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;padding:40px 24px 150px}
|
||
.stagehead{display:flex;align-items:baseline;gap:12px;margin:0 auto 20px;max-width:1120px}
|
||
.stagehead h1{font-family:var(--serif);font-weight:400;font-size:20px;margin:0}.stagehead .sub{color:var(--muted);font-size:13px}
|
||
.stage{max-width:1120px;margin:0 auto}
|
||
.dialog{position:relative;background:var(--sheet);border:1px solid var(--border);border-radius:14px;box-shadow:0 22px 60px -28px rgba(30,27,22,.28),0 2px 8px -4px rgba(30,27,22,.12);overflow:hidden}
|
||
|
||
label{display:block;font-size:12.5px;color:var(--muted);margin-bottom:6px;font-weight:450}
|
||
.req{color:var(--destructive);margin-left:2px}
|
||
input,select,textarea{font-family:var(--sans);font-size:13.5px;color:var(--fg);width:100%;background:var(--sheet);border:1px solid var(--field-border);border-radius:var(--r-sm);height:38px;padding:0 11px;outline:none;transition:border-color .15s,box-shadow .15s}
|
||
textarea{height:auto;padding:9px 11px;resize:vertical;min-height:64px;line-height:1.55}
|
||
select{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='%237c766d' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");background-repeat:no-repeat;background-position:right 10px center;padding-right:30px;cursor:pointer}
|
||
input:focus,select:focus,textarea:focus{border-color:var(--border-strong);box-shadow:var(--ring)}
|
||
.num{text-align:right;font-variant-numeric:tabular-nums}
|
||
|
||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:38px;padding:0 15px;border-radius:var(--r-sm);border:1px solid transparent;background:transparent;font-family:var(--sans);font-size:13.5px;font-weight:500;color:var(--fg);cursor:pointer;transition:.15s;white-space:nowrap}
|
||
.btn svg{width:15px;height:15px}
|
||
.btn.primary{background:var(--fg);color:#fbfaf7;border-color:var(--fg)}.btn.primary:hover{background:#000}
|
||
.btn.secondary{background:var(--beige);color:var(--fg)}.btn.secondary:hover{background:var(--beige-hover)}
|
||
.btn.outline{border-color:var(--field-border);background:var(--sheet)}.btn.outline:hover{background:var(--beige-soft)}
|
||
.btn.ghost{color:var(--muted)}.btn.ghost:hover{background:var(--beige-soft);color:var(--fg)}
|
||
.btn.sm{height:32px;padding:0 11px;font-size:12.5px}
|
||
.iconbtn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--r-sm);border:none;background:transparent;color:var(--muted);cursor:pointer;transition:.15s}
|
||
.iconbtn:hover{background:var(--beige-hover);color:var(--fg)}
|
||
.iconbtn.del:hover{color:var(--destructive)}
|
||
.tabs{display:inline-flex;background:var(--beige-soft);border:1px solid var(--border);border-radius:8px;padding:3px;gap:3px}
|
||
.tabs button{border:none;background:transparent;font-size:13px;font-weight:500;color:var(--muted);padding:6px 14px;border-radius:6px;cursor:pointer;font-family:var(--sans)}
|
||
.tabs button.on{background:var(--sheet);color:var(--fg);box-shadow:0 1px 2px rgba(30,27,22,.08)}
|
||
.seclabel{font-size:11px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
|
||
.pill{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--muted);background:var(--sheet);border:1px solid var(--field-border);border-radius:8px;padding:8px 12px;cursor:pointer;height:36px}
|
||
.pill:hover{background:var(--beige-soft);color:var(--fg)}.pill svg{width:13px;height:13px}
|
||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:13px}.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:13px}
|
||
|
||
.d3top{display:flex;align-items:center;justify-content:space-between;padding:16px 26px 6px}
|
||
.d3top h2{font-family:var(--serif);font-weight:400;font-size:21px;margin:0}
|
||
.d3band{display:flex;align-items:flex-end;gap:26px;padding:10px 26px 16px;border-bottom:1px solid var(--border);flex-wrap:wrap}
|
||
.ul-lab{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);margin-bottom:3px;display:block}
|
||
.ul-cell{display:flex;flex-direction:column;gap:3px}
|
||
.ul-cell input,.ul-cell select{border:0;border-bottom:1px solid var(--field-border);border-radius:0;background:transparent;padding-left:2px;height:34px;min-width:120px}
|
||
.ul-cell input:focus,.ul-cell select:focus{box-shadow:none;border-bottom-color:var(--fg)}
|
||
.work{padding:20px 26px 12px}.addrow{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
|
||
|
||
.menuwrap{position:relative;display:inline-block}
|
||
.menu{position:absolute;right:0;top:36px;z-index:40;min-width:224px;background:var(--sheet);border:1px solid var(--border-strong);border-radius:10px;box-shadow:0 16px 40px -18px rgba(30,27,22,.35);padding:5px;display:none}
|
||
.menu.open{display:block}
|
||
.menu .mlabel{font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--faint);padding:8px 10px 4px}
|
||
.menu button.mi{display:flex;align-items:center;gap:9px;width:100%;background:none;border:none;font-family:var(--sans);font-size:13px;color:var(--fg);text-align:left;padding:8px 10px;border-radius:6px;cursor:pointer;height:auto}
|
||
.menu button.mi:hover{background:var(--beige-soft)}.menu button.mi.danger{color:var(--destructive)}
|
||
.menu .sep{height:1px;background:var(--border);margin:5px 6px}.menu svg{width:15px;height:15px;color:var(--muted)}
|
||
.menu button.mi.danger svg{color:var(--destructive)}.pop{min-width:320px;padding:16px}
|
||
|
||
.rotstrip{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px;padding:9px 11px;background:var(--beige-soft);border-radius:8px}
|
||
.rottag{font-size:11px;font-weight:600;color:var(--muted);font-variant-numeric:tabular-nums}
|
||
.rotstrip select,.rotstrip input{height:32px;background:var(--sheet)}
|
||
.rotstrip .amt{margin-left:auto;font-variant-numeric:tabular-nums;color:var(--success);font-weight:500}
|
||
|
||
/* ===== R4 split rows with FULL aligned header ===== */
|
||
.srow{display:flex;align-items:flex-start;gap:14px;padding:11px 6px}
|
||
.list>.srow{border-top:1px solid var(--border)}
|
||
.srow.line:hover{background:var(--beige-soft);border-radius:8px}
|
||
.headrow{border-bottom:1px solid var(--border);padding-bottom:9px;align-items:flex-end}
|
||
.headrow.srow:hover{background:transparent}
|
||
.hlab{font-size:9.5px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}
|
||
.sgrip{width:14px;flex:none;color:transparent;cursor:grab;padding-top:8px;text-align:center}
|
||
.srow.line:hover .sgrip{color:var(--faint)}
|
||
.sdesc{flex:1;min-width:130px}
|
||
.seco{display:flex;align-items:flex-start;gap:10px}
|
||
.seco .f{display:flex;flex-direction:column;gap:3px}
|
||
.seco .f>.hlab{margin-bottom:0}
|
||
.seco input,.seco select{height:34px;font-size:12.5px}
|
||
.sbel{width:104px;flex:none;text-align:right;font-variant-numeric:tabular-nums;font-family:var(--serif);font-size:16px;padding-top:6px}
|
||
.sact{width:34px;flex:none;display:flex;justify-content:flex-end;gap:2px;padding-top:1px}
|
||
.hovacts .sact{opacity:0;transition:opacity .15s}
|
||
.hovacts .srow.line:hover .sact,.hovacts .sact:has(.menu.open),.hovacts .sact.keep{opacity:1}
|
||
.textrow input{font-style:italic;color:var(--muted)}
|
||
.ghostrow{padding:2px 6px}
|
||
.ghostadd{display:flex;align-items:center;gap:8px;width:100%;background:none;border:none;color:var(--muted);font-family:var(--sans);font-size:13px;padding:10px;cursor:pointer;border-radius:6px;text-align:left}
|
||
.ghostadd:hover{background:var(--beige-soft);color:var(--fg)}.ghostadd svg{width:15px;height:15px}
|
||
|
||
.sumbar{position:sticky;bottom:0;display:flex;align-items:center;gap:20px;padding:15px 26px;border-top:1px solid var(--border-strong);background:rgba(255,255,255,.94);backdrop-filter:blur(6px)}
|
||
.sumbar .brk{color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums;display:flex;gap:14px}
|
||
.sumbar .pay{margin-left:auto;display:flex;align-items:baseline;gap:9px}.sumbar .pay .k{font-size:12px;color:var(--muted)}
|
||
.sumbar .pay .v{font-family:var(--serif);font-size:23px;font-variant-numeric:tabular-nums}
|
||
.sumbar .acts{display:flex;gap:9px;margin-left:8px}
|
||
.warn{display:flex;gap:10px;align-items:flex-start;background:#fbf3ec;border:1px solid #efd9c6;border-radius:8px;padding:11px 13px;font-size:13px;color:#8a5a2f;margin:14px 26px 0}
|
||
.warn svg{width:16px;height:16px;flex:none;margin-top:2px}
|
||
|
||
#picker{position:fixed;right:22px;bottom:22px;z-index:100;width:290px;background:#1c1a17;color:#f4f1ea;border-radius:14px;box-shadow:0 18px 48px -16px rgba(0,0,0,.55);font-family:var(--sans);user-select:none}
|
||
#picker .ph{display:flex;align-items:center;gap:10px;padding:12px 14px;cursor:grab}
|
||
#picker .dot{width:8px;height:8px;border-radius:50%;background:#c9b8a3}#picker .ph b{font-size:12.5px;font-weight:600}#picker .ph .grip{margin-left:auto;color:#6b6459;letter-spacing:2px}
|
||
#picker .nav{display:flex;align-items:center;gap:10px;padding:2px 12px 12px}
|
||
#picker .arrow{width:34px;height:34px;border-radius:8px;border:1px solid #3a352e;background:#26231e;color:#f4f1ea;font-size:16px;cursor:pointer}#picker .arrow:hover{background:#332f28}
|
||
#picker .cur{flex:1;text-align:center}#picker .cur .k{font-size:10.5px;color:#8a8378;letter-spacing:.08em}#picker .cur .n{font-family:var(--serif);font-size:16px}
|
||
#picker .toggles{border-top:1px solid #302c26;padding:11px 12px;display:flex;flex-direction:column;gap:9px}
|
||
#picker .tg{display:flex;align-items:center;gap:8px}#picker .tg .tl{font-size:11px;color:#8a8378;width:44px}
|
||
#picker .seg{display:flex;background:#26231e;border-radius:7px;padding:2px;gap:2px;flex:1}
|
||
#picker .seg button{flex:1;border:none;background:transparent;color:#a9a296;font-size:11.5px;padding:5px 4px;border-radius:5px;cursor:pointer;font-family:var(--sans)}
|
||
#picker .seg button.on{background:#413b32;color:#f7f3ec}
|
||
#picker .hintbar{font-size:10.5px;color:#6b6459;text-align:center;padding:0 12px 11px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="stagehead"><h1>R4 finish — rubrikrad + radåtgärder</h1><span class="sub">kropp låst · header med Beskrivning · lägg-till & ⋮ varieras (2×2)</span></div>
|
||
<div class="stage"><div class="dialog" id="mount"></div></div>
|
||
|
||
<div id="picker">
|
||
<div class="ph" id="pgrip"><span class="dot"></span><b>Finish</b><span class="grip">⠿</span></div>
|
||
<div class="nav"><button class="arrow" id="prev">‹</button><div class="cur"><div class="k" id="curk">F1</div><div class="n" id="curn">Spökrad + ⋮</div></div><button class="arrow" id="next">›</button></div>
|
||
<div class="toggles">
|
||
<div class="tg"><span class="tl">Läge</span><div class="seg" id="segMode"><button data-m="invoice" class="on">Faktura</button><button data-m="self_billed">Självfaktura</button></div></div>
|
||
<div class="tg"><span class="tl">Moms</span><div class="seg" id="segVat"><button data-v="1" class="on">Reg.</button><button data-v="0">Ej reg.</button></div></div>
|
||
</div>
|
||
<div class="hintbar">← / → för att byta variant</div>
|
||
</div>
|
||
|
||
<script>
|
||
const CUSTOMERS=['Nordisk Design AB','Studio Ljus HB','Kaffe & Co AB'];
|
||
const ARTICLES=['Egen rad','KONS-01','DES-04','LIC-12'];
|
||
const UNITS=['tim','st','dag','km','kg','mån'];
|
||
const CUR=['SEK','EUR','USD','GBP','NOK','DKK'];const VAT=['25%','12%','6%','0%'];
|
||
const ROWS=[
|
||
{desc:'Konsultarvode september',qty:'24',unit:'tim',price:'1 150,00',vat:'25%',art:'KONS-01',tot:'27 600,00',rot:false},
|
||
{desc:'Utlägg resa (SJ)',qty:'1',unit:'st',price:'480,00',vat:'25%',art:'Egen rad',tot:'480,00',rot:true},
|
||
];
|
||
let S={vi:0,mode:'invoice',vat:true};
|
||
const isSB=()=>S.mode==='self_billed';
|
||
const opts=(a,s)=>a.map((o,i)=>`<option ${((s==null&&i===0)||o===s)?'selected':''}>${o}</option>`).join('');
|
||
const sel=(a,s)=>`<select>${opts(a,s)}</select>`;
|
||
const icon={
|
||
plus:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>',
|
||
dots:'<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg>',
|
||
trash:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>',
|
||
cal:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M3 9h18M8 2v4M16 2v4"/></svg>',
|
||
bank:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 10l9-6 9 6M5 10v9M19 10v9M9 10v9M15 10v9M3 21h18"/></svg>',
|
||
tag:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3h8l10 10-8 8L3 11V3z"/><circle cx="7" cy="7" r="1.4"/></svg>',
|
||
send:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>',
|
||
chev:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6"/></svg>',
|
||
sliders:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>',
|
||
warn:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l9 16H3l9-16zM12 10v4M12 17.5v.2"/></svg>',
|
||
more:'<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>',
|
||
};
|
||
function rotStrip(){return isSB()?'':`<div class="rotstrip"><span class="rottag">ROT 30%</span>${sel(['Måleri','Snickeri','El','VVS'],'Snickeri')}<input class="num" value="8" style="max-width:70px"><span class="amt">−1 440,00 kr</span></div>`}
|
||
|
||
/* row actions: 'menu' = full ⋮ ; 'inline' = hover trash + compact ⋯ (no delete) */
|
||
function rowActions(style){
|
||
const compact=`<div class="mlabel">Skattereduktion</div><button class="mi">ROT-avdrag</button><button class="mi">RUT-avdrag</button><div class="sep"></div><button class="mi">${icon.cal} Periodisering</button><button class="mi">${icon.bank} Eget försäljningskonto</button><button class="mi">${icon.tag} Dimensioner</button>`;
|
||
if(style==='inline'){
|
||
return `<button class="iconbtn del" title="Ta bort rad">${icon.trash}</button>
|
||
<div class="menuwrap"><button class="iconbtn dotsbtn" title="Mer">${icon.more}</button><div class="menu">${compact}</div></div>`;
|
||
}
|
||
return `<div class="menuwrap"><button class="iconbtn dotsbtn" title="Radåtgärder">${icon.dots}</button><div class="menu">${compact}${isSB()?'':''}<div class="sep"></div><button class="mi danger">${icon.trash} Ta bort rad</button></div></div>`;
|
||
}
|
||
|
||
const FIELDS=()=>[
|
||
{lab:'Artikel',w:120,al:'left',ctrl:r=>sel(ARTICLES,r.art)},
|
||
{lab:'Antal',w:60,al:'right',ctrl:r=>`<input class="num" value="${r.qty}">`},
|
||
{lab:'Enhet',w:78,al:'left',ctrl:r=>sel(UNITS,r.unit)},
|
||
{lab:'à-pris',w:96,al:'right',ctrl:r=>`<input class="num" value="${r.price}">`},
|
||
...(S.vat?[{lab:'Moms',w:70,al:'left',ctrl:r=>sel(VAT,r.vat)}]:[]),
|
||
];
|
||
function headerRow(){
|
||
const eco=FIELDS().map(f=>`<div class="f" style="width:${f.w}px"><span class="hlab" style="text-align:${f.al}">${f.lab}</span></div>`).join('');
|
||
return `<div class="srow headrow">
|
||
<div class="sgrip"></div>
|
||
<div class="sdesc"><span class="hlab">Beskrivning</span></div>
|
||
<div class="seco">${eco}</div>
|
||
<div class="sbel"><span class="hlab">Belopp</span></div>
|
||
<div class="sact"></div>
|
||
</div>`;
|
||
}
|
||
function bodyRow(r,actStyle){
|
||
const eco=FIELDS().map(f=>`<div class="f" style="width:${f.w}px">${f.ctrl(r)}</div>`).join('');
|
||
return `<div class="srow line">
|
||
<div class="sgrip">⠿</div>
|
||
<div class="sdesc"><input value="${r.desc}" placeholder="Beskrivning">${r.rot?rotStrip():''}</div>
|
||
<div class="seco">${eco}</div>
|
||
<div class="sbel">${r.tot}</div>
|
||
<div class="sact ${actStyle==='menu'?'keep':''}">${rowActions(actStyle)}</div>
|
||
</div>`;
|
||
}
|
||
function textRow(actStyle){
|
||
if(isSB())return '';
|
||
return `<div class="srow line">
|
||
<div class="sgrip">⠿</div>
|
||
<div class="sdesc"><input value="Tack för ditt förtroende." style="font-style:italic;color:var(--muted)" placeholder="Fritext (bokförs inte)"></div>
|
||
<div class="seco"></div><div class="sbel"></div>
|
||
<div class="sact ${actStyle==='menu'?'keep':''}">${rowActions(actStyle)}</div>
|
||
</div>`;
|
||
}
|
||
/* add affordance: 'ghost' = inline ghost rows ; 'buttons' = outline buttons below */
|
||
function addAffordance(style){
|
||
if(style==='ghost'){
|
||
return `<div class="ghostrow"><button class="ghostadd">${icon.plus} Lägg till rad</button></div>${isSB()?'':`<div class="ghostrow"><button class="ghostadd">${icon.plus} Lägg till textrad</button></div>`}`;
|
||
}
|
||
return `<div class="addrow"><button class="btn outline sm">${icon.plus} Lägg till rad</button>${isSB()?'':`<button class="btn ghost sm">${icon.plus} Lägg till textrad</button>`}</div>`;
|
||
}
|
||
|
||
function frame(work){
|
||
const mc=(l,c)=>`<div class="ul-cell"><span class="ul-lab">${l}</span>${c}</div>`;
|
||
const meta=`${isSB()?'':mc('Typ',sel(['Faktura','Proforma','Följesedel']))}${mc('Valuta',sel(CUR))}${mc('Datum','<input type="date" value="2026-07-21">')}${mc('Förfaller','<input type="date" value="2026-08-20">')}
|
||
<div class="menuwrap"><button class="pill dotsbtn">${icon.sliders} Referenser & mer ${icon.chev}</button>
|
||
<div class="menu pop"><div style="display:flex;flex-direction:column;gap:14px">
|
||
${isSB()?`<div><label>Avtalsreferens (självfakturering)</label><input placeholder="SB-2026-014"></div>`:`<div class="grid2"><div><label>Er referens</label><input placeholder="Kundens ref"></div><div><label>Vår referens</label><input value="Emil Mattsson"></div></div>`}
|
||
${isSB()?'':`<div><div class="seclabel" style="margin-bottom:8px">ROT/RUT-uppgifter</div><div class="grid3"><div><label>Fastighetsbet.</label><input placeholder="Gårda 12:3"></div><div><label>Lgh.nr</label><input placeholder="1101"></div><div><label>BRF org.nr</label><input placeholder="769600-0000"></div></div></div>`}
|
||
<div><label>Anteckningar</label><textarea>Betalning inom 30 dagar. Dröjsmålsränta enligt räntelagen.</textarea></div>
|
||
</div></div></div>`;
|
||
const send=isSB()?'':`<button class="btn secondary">${icon.send} Skicka</button>`;
|
||
return `<div class="d3top"><h2>${isSB()?'Ny självfaktura':'Ny faktura'}</h2>
|
||
<div class="tabs"><button class="${!isSB()?'on':''}" onclick="setMode('invoice')">Faktura</button><button class="${isSB()?'on':''}" onclick="setMode('self_billed')">Självfaktura</button></div></div>
|
||
<div class="d3band">
|
||
<div style="flex:1;min-width:0;max-width:340px"><span class="ul-lab">${isSB()?'Utställare':'Kund'}</span>
|
||
<div style="display:flex;gap:8px;align-items:center"><div style="flex:1">${sel(['Välj kund…',...CUSTOMERS],'Nordisk Design AB')}</div><button class="btn ghost sm">${icon.plus}</button></div></div>
|
||
<div style="display:flex;gap:20px;align-items:flex-end;flex-wrap:wrap">${meta}</div>
|
||
</div>
|
||
${isSB()?'':`<div class="warn">${icon.warn}<div>Inga bankuppgifter inlagda ännu. Fakturan kan skapas men saknar betalinfo.</div></div>`}
|
||
${work}
|
||
<div class="sumbar"><div class="brk"><span>Delsumma 28 080,00</span>${S.vat?'<span>Moms 7 020,00</span>':''}<span>ROT −1 440,00</span></div>
|
||
<div class="pay"><span class="k">Att betala</span><span class="v">${S.vat?'33 660,00':'26 640,00'} kr</span></div>
|
||
<div class="acts"><button class="btn ghost">Spara utkast</button>${send}<button class="btn primary">${isSB()?'Registrera självfaktura':'Granska & skapa'}</button></div>
|
||
</div>`;
|
||
}
|
||
|
||
function build(addStyle,actStyle){
|
||
const rows=ROWS.map(r=>bodyRow(r,actStyle)).join('')+textRow(actStyle);
|
||
const listCls=actStyle==='inline'?'list hovacts':'list';
|
||
const work=`<div class="work"><div class="seclabel" style="margin-bottom:6px">Fakturarader</div>
|
||
${headerRow()}
|
||
<div class="${listCls}">${rows}</div>
|
||
${addAffordance(addStyle)}
|
||
</div>`;
|
||
return frame(work);
|
||
}
|
||
const VARIANTS=[
|
||
{k:'F1',n:'Spökrad + ⋮',r:()=>build('ghost','menu')},
|
||
{k:'F2',n:'Knappar + ⋮',r:()=>build('buttons','menu')},
|
||
{k:'F3',n:'Spökrad + hover-åtgärder',r:()=>build('ghost','inline')},
|
||
{k:'F4',n:'Knappar + hover-åtgärder',r:()=>build('buttons','inline')},
|
||
];
|
||
const mount=document.getElementById('mount');
|
||
function render(){const V=VARIANTS[S.vi];mount.innerHTML=V.r();document.getElementById('curk').textContent=V.k;document.getElementById('curn').textContent=V.n;wire();}
|
||
function setMode(m){S.mode=m;document.querySelectorAll('#segMode button').forEach(b=>b.classList.toggle('on',b.dataset.m===m));render();}
|
||
function step(d){S.vi=(S.vi+d+VARIANTS.length)%VARIANTS.length;render();}
|
||
document.getElementById('prev').onclick=()=>step(-1);document.getElementById('next').onclick=()=>step(1);
|
||
document.querySelectorAll('#segMode button').forEach(b=>b.onclick=()=>setMode(b.dataset.m));
|
||
document.querySelectorAll('#segVat button').forEach(b=>b.onclick=()=>{S.vat=b.dataset.v==='1';document.querySelectorAll('#segVat button').forEach(x=>x.classList.toggle('on',x===b));render();});
|
||
document.addEventListener('keydown',e=>{const t=e.target.tagName;if(t==='INPUT'||t==='SELECT'||t==='TEXTAREA'||e.target.isContentEditable)return;if(e.key==='ArrowLeft')step(-1);if(e.key==='ArrowRight')step(1);});
|
||
function wire(){document.querySelectorAll('.dotsbtn').forEach(btn=>btn.onclick=e=>{e.stopPropagation();const m=btn.parentElement.querySelector('.menu');const open=m.classList.contains('open');closeMenus();m.classList.toggle('open',!open);const s=btn.closest('.sact');if(s)s.classList.toggle('keep',!open);});}
|
||
function closeMenus(){document.querySelectorAll('.menu.open').forEach(m=>m.classList.remove('open'));document.querySelectorAll('.sact.keep').forEach(s=>{if(!s.querySelector('.rowActions'))return;});}
|
||
document.addEventListener('click',()=>{document.querySelectorAll('.menu.open').forEach(m=>m.classList.remove('open'));});
|
||
(function(){const p=document.getElementById('picker'),h=document.getElementById('pgrip');let dx,dy,drag=false;
|
||
h.addEventListener('pointerdown',e=>{drag=true;const r=p.getBoundingClientRect();dx=e.clientX-r.left;dy=e.clientY-r.top;h.setPointerCapture(e.pointerId)});
|
||
h.addEventListener('pointermove',e=>{if(!drag)return;p.style.left=(e.clientX-dx)+'px';p.style.top=(e.clientY-dy)+'px';p.style.right='auto';p.style.bottom='auto'});
|
||
h.addEventListener('pointerup',()=>drag=false);})();
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|