Files
boc/landvex-finance-sprint3/js/utils.mjs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

219 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ── 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} ${['JanMar', 'AprJun', 'JulSep', 'OktDec'][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);
}