bae705aa97
- 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
219 lines
5.5 KiB
JavaScript
219 lines
5.5 KiB
JavaScript
/**
|
||
* 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, '>')
|
||
.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 '<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>';
|
||
}
|
||
|
||
// ── 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);
|
||
}
|