/** * LandveX Finance — Utilities * Sprint 3: UI-refaktorering */ export const MN = ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec']; // ── Formatering ── export function fmt(v) { const a = Math.abs(v); return (v < 0 ? '−' : '') + Math.round(a).toLocaleString('sv-SE') + ' kr'; } export function fmtNumber(v, decimals = 0) { return Number(v).toLocaleString('sv-SE', { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }); } export function fmtPercent(v, decimals = 1) { return Number(v).toLocaleString('sv-SE', { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }) + ' %'; } export function fmtDate(d) { if (!d) return '—'; const date = new Date(d); return date.toLocaleDateString('sv-SE'); } // ── Escape ── export function esc(s) { return String(s || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ── Datum ── export function today() { return new Date().toISOString().slice(0, 10); } export function now() { return new Date(); } export function curPeriod() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; } // ── Period-hjälpare ── export function pLabel(p) { if (!p) return '—'; if (/^Q[1-4]-\d{4}$/.test(p)) { const q = p[1]; const y = p.split('-')[1]; return `Q${q} ${['Jan–Mar', 'Apr–Jun', 'Jul–Sep', 'Okt–Dec'][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}`; } export function pParam(p) { if (!p) return ''; if (/^Q[1-4]-\d{4}$/.test(p)) { const q = parseInt(p[1]) - 1; const 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}`; } // ── Konto-hjälpare ── export function acname(a) { return a?.account_name || a?.name || ''; } export function fbal(a) { return parseFloat(a?.balance || 0); } export function fdeb(a) { return parseFloat(a?.total_debit || a?.debit || 0); } export function fcrd(a) { return parseFloat(a?.total_credit || a?.credit || 0); } // ── Kvitto-hjälpare ── 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']; export 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])); } export function rcptBadge(e) { if (!needsReceipt(e)) { return '☑ auto'; } const n = (e?.metadata?.receipts || []).length; return n > 0 ? `✅ ${n} kvitto${n > 1 ? 'n' : ''}` : '❌ Saknar'; } // ── DOM-hjälpare ── export function $(id) { return document.getElementById(id); } export function $$$(selector, context = document) { return context.querySelector(selector); } export function $$$(selector, context = document) { return Array.from(context.querySelectorAll(selector)); } export function empty(el) { if (typeof el === 'string') el = $(el); if (el) el.innerHTML = ''; } export function show(el) { if (typeof el === 'string') el = $(el); if (el) el.style.display = ''; } export function hide(el) { if (typeof el === 'string') el = $(el); if (el) el.style.display = 'none'; } // ── Event-hjälpare ── export function on(el, event, fn) { if (typeof el === 'string') el = $(el); if (el) el.addEventListener(event, fn); } export function off(el, event, fn) { if (typeof el === 'string') el = $(el); if (el) el.removeEventListener(event, fn); } // ── Debounce ── export function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } // ── Throttle ── export function throttle(fn, ms) { let last = 0; return (...args) => { const now = Date.now(); if (now - last >= ms) { last = now; fn(...args); } }; } // ── LocalStorage ── export function lsGet(key, fallback = null) { try { const v = localStorage.getItem(`landvex:${key}`); return v ? JSON.parse(v) : fallback; } catch { return fallback; } } export function lsSet(key, value) { try { localStorage.setItem(`landvex:${key}`, JSON.stringify(value)); } catch { /* ignore */ } } // ── Download helper ── export function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } export function downloadText(text, filename, type = 'text/plain') { const blob = new Blob([text], { type }); downloadBlob(blob, filename); }