/* Účtovníček — spoločné pomôcky (formátovanie, ikonky, toast, potvrdenie) */
const fmt = {
eur: (n) => (n == null || isNaN(n)) ? '—' : new Intl.NumberFormat('sk-SK', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2 }).format(n),
num: (n, d = 2) => (n == null || isNaN(n)) ? '—' : new Intl.NumberFormat('sk-SK', { minimumFractionDigits: d, maximumFractionDigits: d }).format(n),
qty: (n) => (n == null || isNaN(n)) ? '—' : new Intl.NumberFormat('sk-SK', { maximumFractionDigits: 3 }).format(n),
date: (s) => {
if (!s) return '—';
const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!m) return s;
return `${+m[3]}. ${+m[2]}. ${m[1]}`;
},
dateLong: (d) => new Intl.DateTimeFormat('sk-SK', { weekday: 'long', day: 'numeric', month: 'long' }).format(d),
};
function plural(n, one, few, many) {
const k = Math.abs(+n || 0);
if (k === 1) return one;
if (k >= 2 && k <= 4) return few;
return many;
}
const STATUS = {
vystavena: { label: 'Neuhradená', cls: 'orange' },
ciastocne: { label: 'Čiastočne uhradená', cls: 'blue' },
uhradena: { label: 'Uhradená', cls: 'green' },
storno: { label: 'Stornovaná', cls: 'gray' },
};
function StatusBadge({ inv }) {
if (uctStore.invoices.isOverdue(inv)) return Po splatnosti;
const s = STATUS[inv.status] || { label: inv.status, cls: 'gray' };
return {s.label};
}
/* Čiarové ikonky (rovnaká sada ako Mzdárik + pár účtovných) */
function Icon({ name, size = 16, className = '' }) {
const props = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', className };
const paths = {
dashboard: <>>,
users: <>>,
file: <>>,
fileIn: <>>,
bank: <>>,
wallet: <>>,
book: <>>,
settings: <>>,
plus: <>>,
edit: <>>,
trash: <>>,
download: <>>,
upload: <>>,
print: <>>,
chevronRight: <>>,
x: <>>,
check: <>>,
search: <>>,
info: <>>,
clock: <>>,
coins: <>>,
list: <>>,
ledger: <>>,
scale: <>>,
grid: <>>,
arrowDown: <>>,
arrowUp: <>>,
building: <>>,
};
return ;
}
function useToast() {
const [msg, setMsg] = React.useState(null);
React.useEffect(() => {
if (!msg) return;
const t = setTimeout(() => setMsg(null), 2600);
return () => clearTimeout(t);
}, [msg]);
const Toast = msg ?
{msg}
: null;
return [setMsg, Toast];
}
/* Potvrdenie v štýle appky (prevzaté z Mzdárika). askConfirm vracia Promise. */
function useConfirm() {
const [req, setReq] = React.useState(null);
const askConfirm = React.useCallback((opts) => new Promise((resolve) => {
const base = typeof opts === 'string' ? { message: opts } : (opts || {});
setReq({ title: 'Potvrdenie', confirmLabel: 'Potvrdiť', cancelLabel: 'Zrušiť', danger: false, ...base, resolve });
}), []);
const close = (value) => { if (!req) return; req.resolve(value); setReq(null); };
React.useEffect(() => {
if (!req) return;
const onKey = (e) => {
if (e.key === 'Escape') { e.preventDefault(); req.resolve(false); setReq(null); }
if (e.key === 'Enter') { e.preventDefault(); req.resolve(true); setReq(null); }
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [req]);
const ConfirmUI = req ? (
close(false)}>
e.stopPropagation()} role="alertdialog" aria-modal="true">
{req.title}
{req.message}
) : null;
return [askConfirm, ConfirmUI];
}
/* Desatinné pole bez neduhov type=number (prevzaté z Mzdárika). Prijíma čiarku aj bodku. */
function DecimalInput({ value, onCommit, className, ...rest }) {
const fromValue = (v) => (v === '' || v == null || (typeof v === 'number' && isNaN(v))) ? '' : String(v).replace('.', ',');
const [text, setText] = React.useState(() => fromValue(value));
const [editing, setEditing] = React.useState(false);
React.useEffect(() => { if (!editing) setText(fromValue(value)); }, [value, editing]);
const handle = (e) => {
let s = String(e.target.value);
const neg = s.trim().startsWith('-');
s = s.replace(/[^0-9.,]/g, '').replace(/\./g, ',');
const parts = s.split(',');
if (parts.length > 2) s = parts[0] + ',' + parts.slice(1).join('');
s = (neg ? '-' : '') + s;
setText(s);
const n = parseFloat(s.replace(',', '.'));
onCommit(isNaN(n) ? 0 : n);
};
return { setEditing(true); e.target.select(); }}
onBlur={() => { setEditing(false); setText(fromValue(value)); }}
onChange={handle} />;
}
function Stat({ label, value, delta, muted, dir }) {
return (
{label}
{value}
{delta &&
{delta}
}
);
}
/* Bočný panel (drawer) ako v Mzdáriku. */
function Drawer({ title, sub, onClose, footer, children, wide }) {
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
return (
e.stopPropagation()}>
{children}
{footer &&
{footer}
}
);
}
function Field({ label, hint, children, style }) {
return (
{label &&
}
{children}
{hint &&
{hint}
}
);
}
/* Chybová hláška pre formuláre - vždy povie, čo sa stalo. */
function ErrorNote({ error, onClose }) {
if (!error) return null;
return (
Nepodarilo sa uložiť.{error}
{onClose &&
}
);
}
/* Výber účtu z osnovy: píšeš číslo alebo názov, ponúkne sa zoznam (datalist). */
function AccountPicker({ value, onChange, prefixes, placeholder, style, className }) {
const listId = React.useMemo(() => 'acc-dl-' + Math.random().toString(36).slice(2), []);
const accounts = prefixes && prefixes.length ? uctStore.accounts.byClass(...prefixes) : uctStore.accounts.list();
const name = uctStore.accounts.name(value);
return (
onChange(e.target.value.replace(/\s.*$/, '').trim())} title={name} />
{value &&
{name || 'účet nie je v osnove'}
}
);
}
/* Výber partnera: select + rýchle pridanie. onPick dostane partnera alebo null. */
function PartnerSelect({ value, onPick, allowNew = true, placeholder = '— vyber partnera —' }) {
const [adding, setAdding] = React.useState(false);
const partners = uctStore.partners.list();
return (
{allowNew &&
}
{adding &&
setAdding(false)} onSaved={(p) => { setAdding(false); onPick(p); }} />}
);
}
/* Spinner / načítavam */
function Loading({ text = 'Načítavam…' }) {
return {text}
;
}
window.fmt = fmt;
window.Icon = Icon;
window.plural = plural;
window.STATUS = STATUS;