/**
* 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);
}