/* Účtovníček — Banka (výpisy), Pokladňa (PPD/VPD), Interné doklady: zoznam, formulár s riadkami, detail */
const DOC_META = {
bank: { title: 'Banka', sub: 'Bankové výpisy - každý riadok je platba s protiúčtom, úhrady faktúr sa spárujú podľa VS', one: 'výpis', newLabel: 'Nový výpis', icon: 'bank', binder: 'BAN' },
cash: { title: 'Pokladňa', sub: 'Príjmové (PPD) a výdavkové (VPD) pokladničné doklady', one: 'doklad', newLabel: 'Nový pokladničný doklad', icon: 'wallet', binder: 'POK' },
internal: { title: 'Interné doklady', sub: 'Zápisy, ktoré nemajú faktúru ani platbu: mzdy, odpisy, dane, začiatočné stavy', one: 'doklad', newLabel: 'Nový interný doklad', icon: 'book', binder: 'ID' },
};
function PageDocuments({ kind, go, route }) {
const meta = DOC_META[kind];
const [, action, routeId] = (route || '').split(':');
const [openId, setOpenId] = React.useState(action === 'open' ? routeId : null);
const [creating, setCreating] = React.useState(action === 'new');
const [search, setSearch] = React.useState('');
const [year, setYear] = React.useState('');
const [setToast, Toast] = useToast();
React.useEffect(() => {
if (action === 'open') setOpenId(routeId);
if (action === 'new') setCreating(true);
}, [route]);
const all = uctStore.documents.list(kind);
const years = [...new Set(all.map(d => String(d.date).slice(0, 4)))].sort().reverse();
const q = search.trim().toLowerCase();
const list = all.filter(d => (!year || String(d.date).startsWith(year)) && (!q || d.number.toLowerCase().includes(q) || (d.title || '').toLowerCase().includes(q) || (d.partnerName || '').toLowerCase().includes(q) || d.lines.some(l => (l.text || '').toLowerCase().includes(q) || (l.vs || '').includes(q))));
const sum = (f) => list.reduce((a, d) => a + (f(d) || 0), 0);
const balance = kind === 'bank' ? uctStore.dashboard.get().bank : kind === 'cash' ? uctStore.dashboard.get().cash : null;
return (
{balance != null &&
Zostatok
{fmt.eur(balance)} }
setYear(e.target.value)} style={{width:110}}>
všetky roky {years.map(y => {y} )}
setSearch(e.target.value)} />
setCreating(true)}> {meta.newLabel}
Číslo Dátum {kind === 'cash' ? 'Účel' : kind === 'bank' ? 'Výpis' : 'Popis'} Partner
{kind === 'internal' ? Suma : <>Príjem Výdaj >}
{list.map(d => (
setOpenId(d.id)}>
{d.number} {kind === 'cash' && {d.cashType === 'vydaj' ? 'výdaj' : 'príjem'} }{d.diff ? neúplný : null}
{fmt.date(d.date)}
{d.title || (d.lines[0] && d.lines[0].text) || {d.lines.length} {plural(d.lines.length, 'riadok', 'riadky', 'riadkov')} }
{d.partnerName}
{kind === 'internal' ? {fmt.eur(d.total)} : <>{d.income ? fmt.num(d.income) : ''} {d.expense ? fmt.num(d.expense) : ''} >}
))}
{list.length === 0 && {all.length ? 'Nič sa nenašlo' : `Zatiaľ žiadny ${meta.one}`} {all.length ? 'Skús iný rok alebo hľadanie.' : `Pridaj prvý tlačidlom „${meta.newLabel}" vpravo hore.`}
}
{list.length > 0 && (
{list.length} {plural(list.length, meta.one, kind === 'bank' ? 'výpisy' : 'doklady', kind === 'bank' ? 'výpisov' : 'dokladov')}
{kind === 'internal' ? {fmt.eur(sum(d => d.total))} : <>{fmt.num(sum(d => d.income))} {fmt.num(sum(d => d.expense))} >}
)}
{creating &&
{ setCreating(false); go(kind); }} onSaved={(d) => { setCreating(false); setOpenId(d.id); setToast(`${d.number} uložený.`); }} />}
{openId && !creating && { setOpenId(null); go(kind); }} setToast={setToast} go={go} />}
{Toast}
);
}
/* Príjem/výdaj v riadku: protiúčet DAL = peniaze prišli, protiúčet MD = peniaze odišli. */
function lineIncome(l) { return l.dal || 0; }
function lineExpense(l) { return l.md || 0; }
function DocumentForm({ kind, document, onClose, onSaved }) {
const isNew = !document;
const meta = DOC_META[kind];
const [d, setD] = React.useState(() => isNew ? uctStore.documents.blank(kind) : structuredClone(document));
const [preview, setPreview] = React.useState('…');
const [error, setError] = React.useState(null);
const [saving, setSaving] = React.useState(false);
const money = kind !== 'internal';
React.useEffect(() => {
if (!isNew) return;
const y = +String(d.date || '').slice(0, 4) || new Date().getFullYear();
const sk = kind === 'bank' ? 'bank' : kind === 'cash' ? (d.cashType === 'vydaj' ? 'cashout' : 'cashin') : 'internal';
uctStore.documents.nextNumber(sk, y).then(r => setPreview(r.number)).catch(() => setPreview('?'));
}, [d.date, d.cashType, isNew]);
const set = (k, v) => setD(prev => ({ ...prev, [k]: v }));
const setLine = (i, patch) => setD(prev => ({ ...prev, lines: prev.lines.map((l, j) => j === i ? { ...l, ...patch } : l) }));
const addLine = () => set('lines', [...d.lines, { text: '', account: '', md: 0, dal: 0, partnerId: '', partnerName: '', vs: '' }]);
const delLine = (i) => set('lines', d.lines.length > 1 ? d.lines.filter((_, j) => j !== i) : d.lines);
const pickLinePartner = (i, p) => setLine(i, { partnerId: p ? p.id : '', partnerName: p ? p.name : '' });
const income = d.lines.reduce((a, l) => a + lineIncome(l), 0);
const expense = d.lines.reduce((a, l) => a + lineExpense(l), 0);
const sumMD = d.lines.reduce((a, l) => a + (l.md || 0), 0);
const sumDAL = d.lines.reduce((a, l) => a + (l.dal || 0), 0);
/* Napovedanie faktúry podľa VS: keď riadok má VS neuhradenej faktúry, ponúkni sumu a účet. */
const matchInvoice = (vs) => {
if (!vs) return null;
const kindWanted = kind === 'cash' && d.cashType === 'vydaj' ? 'purchase' : null;
return uctStore.invoices.all().find(i => i.vs === vs && i.status !== 'storno' && (!kindWanted || i.kind === kindWanted)) || null;
};
const applyInvoice = (i, inv) => {
const rest = uctStore.invoices.rest(inv);
const patch = { partnerId: inv.partnerId, partnerName: inv.partnerName, account: inv.account, text: patchText(d.lines[i].text, `Úhrada ${inv.number}`) };
if (inv.kind === 'sales') { patch.dal = rest; patch.md = 0; } else { patch.md = rest; patch.dal = 0; }
setLine(i, patch);
};
const patchText = (cur, def) => cur && cur.trim() ? cur : def;
const save = async () => {
setError(null);
if (d.lines.some(l => !l.account)) { setError('Každý riadok potrebuje účet.'); return; }
if (!money && Math.round((sumMD - sumDAL) * 100) !== 0) { setError(`Interný doklad nie je vyrovnaný: MD ${fmt.num(sumMD)}, DAL ${fmt.num(sumDAL)}.`); return; }
setSaving(true);
try {
const saved = isNew ? await uctStore.documents.create(d) : await uctStore.documents.update(d);
onSaved(saved);
} catch (e) { setError(e.message); }
setSaving(false);
};
return (
Dostane číslo {preview} > : null}
onClose={onClose}
footer={<>
Zrušiť
{isNew ? 'Uložiť a zaúčtovať' : 'Uložiť zmeny'}
>}>
setError(null)} />
set('date', e.target.value)} />
{kind === 'cash' && (
set('cashType', e.target.value)}>Príjmový (PPD) - peniaze do pokladne Výdavkový (VPD) - peniaze z pokladne
)}
{money &&
set('moneyAccount', v)} prefixes={[kind === 'bank' ? '22' : '21']} /> }
{kind !== 'bank' &&
setD(prev => ({ ...prev, partnerId: p ? p.id : '', partnerName: p ? p.name : '' }))} /> }
set('title', e.target.value)} placeholder={kind === 'bank' ? 'Výpis č. 8 za august' : kind === 'cash' ? 'Cestovné náhrady - služobná cesta Žilina' : 'Mzdy 10/2026'} />
{money ? 'Pohyby (riadky výpisu)' : 'Zápisy (MD / DAL)'}
Pridať riadok
{money ? <>
Príjmy {fmt.eur(income)}
Výdaje {fmt.eur(expense)}
Zmena zostatku {fmt.eur(income - expense)}
> : <>
MD spolu {fmt.eur(sumMD)}
DAL spolu {fmt.eur(sumDAL)}
Rozdiel 0.004 ? 'var(--danger)' : 'var(--accent)'}}>{fmt.eur(sumMD - sumDAL)}
>}
{kind === 'bank' && 'Riadok s účtom 311… a VS faktúry sa po uložení spáruje s faktúrou a tá bude uhradená. Zadaj VS a klikni na ponúknuté číslo faktúry - doplní sa partner, účet aj suma.'}
{kind === 'cash' && 'Príjmový doklad: protiúčet je napr. 602… (tržba) alebo 365… (vklad spoločníka). Výdavkový: napr. 512… (cestovné), 501… (materiál).'}
{kind === 'internal' && 'Každý riadok má účet a sumu na strane MD alebo DAL. Doklad musí byť vyrovnaný: súčet MD = súčet DAL.'}
);
}
function DocumentDetail({ id, kind, onClose, setToast, go }) {
const d = uctStore.documents.get(id);
const meta = DOC_META[kind];
const [editing, setEditing] = React.useState(false);
const [error, setError] = React.useState(null);
const [entries, setEntries] = React.useState(null);
const [askConfirm, ConfirmUI] = useConfirm();
React.useEffect(() => { if (d) uctStore.journal.list({ doc: d.id }).then(setEntries).catch(() => setEntries([])); }, [id, d && d.updatedAt]);
if (!d) return null;
const money = kind !== 'internal';
const remove = async () => {
if (!await askConfirm({ title: `Vymazať ${d.number}?`, message: 'Zmiznú aj jeho zápisy v denníku a úhrady faktúr, ktoré z neho vznikli. Číslo v rade ostane preskočené.', confirmLabel: 'Vymazať', danger: true })) return;
try { await uctStore.documents.remove(d.id); setToast('Doklad vymazaný.'); onClose(); } catch (e) { setError(e.message); }
};
const invoiceFor = (vs) => vs ? uctStore.invoices.all().find(i => i.vs === vs) : null;
if (editing) return setEditing(false)} onSaved={() => { setEditing(false); setToast('Doklad uložený.'); }} />;
return (
{d.number}}
sub={<>{kind === 'cash' && {d.cashType === 'vydaj' ? 'výdavkový' : 'príjmový'} } {fmt.date(d.date)}{d.source === 'profit365' ? ' · prenesený z Profit365' : ''} >}
onClose={onClose}
footer={<>
Vymazať
go('journal:doc:' + d.id)}> V denníku
setEditing(true)}> Upraviť
>}>
setError(null)} />
{d.diff ? V Profit365 tento výpis nemal zaúčtované všetky riadky: pohyb na účte {d.moneyAccount} sa od riadkov líši o {fmt.eur(d.diff)} . Denník je prenesený tak, ako bol - doplň chýbajúce riadky cez Upraviť.
: null}
{kind === 'cash' ? 'Účel' : 'Popis'}
{d.title || '—'}
{money &&
{kind === 'bank' ? 'Účet banky' : 'Účet pokladne'}
{d.moneyAccount} {uctStore.accounts.name(d.moneyAccount)}
}
{d.partnerName &&
}
{money &&
Príjmy
{fmt.eur(d.income)}
}
{money &&
Výdaje
{fmt.eur(d.expense)}
}
{!money &&
}
Text {money ? 'Protiúčet' : 'Účet'} Partner VS {money ? 'Príjem' : 'MD'} {money ? 'Výdaj' : 'DAL'}
{d.lines.map((l, i) => {
const inv = invoiceFor(l.vs);
return (
{l.text}
{l.account} {uctStore.accounts.name(l.account)}
{l.partnerName}
{l.vs}{inv && go((inv.kind === 'purchase' ? 'purchase' : 'invoices') + ':open:' + inv.id)}>{inv.number} }
{(money ? l.dal : l.md) ? fmt.num(money ? l.dal : l.md) : ''}
{(money ? l.md : l.dal) ? fmt.num(money ? l.md : l.dal) : ''}
);
})}
{d.note && {d.note}
}
Zaúčtovanie v denníku
{entries === null ?
: (
Účet Text MD DAL
{entries.map(e => {e.account} {uctStore.accounts.name(e.account)} {e.text} {e.md ? fmt.num(e.md) : ''} {e.dal ? fmt.num(e.dal) : ''} )}
)}
{ConfirmUI}
);
}