/* Účtovníček — Vystavené faktúry: zoznam, formulár, detail s úhradami, tlač */ const INV_META = { sales: { title: 'Vystavené faktúry', series: 'VF1', partner: 'Odberateľ', newLabel: 'Nová faktúra', route: 'invoices', verb: 'vystavená', doc: 'Faktúra' }, proforma: { title: 'Zálohové faktúry', series: 'VF2', partner: 'Odberateľ', newLabel: 'Nová zálohová faktúra', route: 'proforma', verb: 'vystavená', doc: 'Zálohová faktúra' }, purchase: { title: 'Prijaté faktúry', series: 'DFA', partner: 'Dodávateľ', newLabel: 'Nová prijatá faktúra', route: 'purchase', verb: 'prijatá', doc: 'Faktúra' }, }; /* Účty (predkontácia) sa ukazujú len v podvojnom účtovníctve. */ const showAccounting = () => uctStore.ctx.mode() === 'podvojne'; function PageInvoices({ kind = 'sales', go, route }) { const meta = INV_META[kind]; const [, action, routeId] = (route || '').split(':'); const [filter, setFilter] = React.useState('all'); const [search, setSearch] = React.useState(''); const [openId, setOpenId] = React.useState(action === 'open' ? routeId : null); const [creating, setCreating] = React.useState(action === 'new'); const [setToast, Toast] = useToast(); React.useEffect(() => { if (action === 'open') setOpenId(routeId); if (action === 'new') setCreating(true); }, [route]); const all = uctStore.invoices.list(kind); const q = search.trim().toLowerCase(); const list = all.filter(i => { if (filter === 'unpaid' && !(i.status === 'vystavena' || i.status === 'ciastocne')) return false; if (filter === 'paid' && i.status !== 'uhradena') return false; if (filter === 'overdue' && !uctStore.invoices.isOverdue(i)) return false; return !q || i.number.toLowerCase().includes(q) || i.partnerName.toLowerCase().includes(q) || (i.vs || '').includes(q) || (i.supplierNumber || '').toLowerCase().includes(q); }); const sum = (arr, f) => arr.reduce((a, i) => a + (f(i) || 0), 0); const counts = { all: all.length, unpaid: all.filter(i => i.status === 'vystavena' || i.status === 'ciastocne').length, paid: all.filter(i => i.status === 'uhradena').length, overdue: all.filter(uctStore.invoices.isOverdue).length, }; return (

{meta.title}

rad {meta.series} · {all.length} {plural(all.length, 'faktúra', 'faktúry', 'faktúr')} · neuhradené {fmt.eur(sum(all.filter(i => i.status !== 'storno'), uctStore.invoices.rest))}{kind === 'proforma' ? ' · zálohová faktúra sa neúčtuje, účtuje sa až riadna faktúra a platba' : ''}
setSearch(e.target.value)} />
{[['all', 'Všetky'], ['unpaid', 'Neuhradené'], ['overdue', 'Po splatnosti'], ['paid', 'Uhradené']].map(([k, l]) => ( ))}
{list.map(inv => ( setOpenId(inv.id)}> ))} {list.length === 0 && } {list.length > 0 && ( )}
Číslo{meta.partner}{kind === 'purchase' ? 'Prijatá' : 'Vystavená'}SplatnosťVSStavSumaZostáva
{inv.number} {inv.partnerName}{inv.demo && ukážka}{kind === 'purchase' && inv.supplierNumber && inv.supplierNumber !== inv.vs &&
č. dod. {inv.supplierNumber}
}
{fmt.date(inv.issueDate)} {fmt.date(inv.dueDate)} {inv.vs} {fmt.eur(inv.total)} {inv.status === 'uhradena' ? 0,00 € : fmt.eur(uctStore.invoices.rest(inv))}

{all.length ? 'Nič tu nie je' : 'Zatiaľ žiadne faktúry'}

{all.length ? 'Skús iný filter alebo hľadanie.' : (kind === 'sales' ? 'Vystav prvú faktúru tlačidlom vpravo hore.' : 'Zapíš prvú prijatú faktúru tlačidlom vpravo hore.')}
{list.length} {plural(list.length, 'faktúra', 'faktúry', 'faktúr')} {fmt.eur(sum(list, i => i.total))} {fmt.eur(sum(list.filter(i => i.status !== 'storno'), uctStore.invoices.rest))}
{creating && { setCreating(false); go(meta.route); }} onSaved={(inv) => { setCreating(false); setOpenId(inv.id); setToast(kind === 'purchase' ? `Faktúra ${inv.number} zapísaná.` : `Faktúra ${inv.number} vystavená.`); }} />} {openId && !creating && { setOpenId(null); go(meta.route); }} setToast={setToast} go={go} />} {Toast}
); } /* --- Formulár (nová aj úprava) --- */ function InvoiceForm({ kind = 'sales', invoice, onClose, onSaved }) { const isNew = !invoice; if (invoice) kind = invoice.kind; const meta = INV_META[kind]; const sales = kind !== 'purchase'; const acc = showAccounting(); const [inv, setInv] = React.useState(() => isNew ? uctStore.invoices.blank(kind) : structuredClone(invoice)); const [showAccounts, setShowAccounts] = React.useState(() => acc && kind === 'purchase'); const [preview, setPreview] = React.useState({ number: '…', vs: '' }); const [error, setError] = React.useState(null); const [saving, setSaving] = React.useState(false); const [addingPartner, setAddingPartner] = React.useState(false); const partners = uctStore.partners.list(); const company = uctStore.company.get(); React.useEffect(() => { if (!isNew) return; const y = +String(inv.issueDate || '').slice(0, 4) || new Date().getFullYear(); uctStore.invoices.nextNumber(kind, y).then(setPreview).catch(() => setPreview({ number: '?', vs: '' })); }, [inv.issueDate, isNew]); const set = (k, v) => setInv(prev => ({ ...prev, [k]: v })); const pickPartner = (id) => { const p = uctStore.partners.get(id); if (!p) { setInv(prev => ({ ...prev, partnerId: '', partnerName: '', partnerIco: '', partnerDic: '', partnerIcDph: '', partnerAddress: '' })); return; } setInv(prev => ({ ...prev, partnerId: p.id, partnerName: p.name, partnerIco: p.ico || '', partnerDic: p.dic || '', partnerIcDph: p.icDph || '', partnerAddress: uctStore.partners.addressLine(p) })); }; const setRow = (i, k, v) => setInv(prev => { const rows = prev.rows.map((r, j) => j === i ? { ...r, [k]: v } : r); rows[i].total = Math.round((rows[i].qty || 0) * (rows[i].unitPrice || 0) * 100) / 100; return { ...prev, rows }; }); const addRow = () => set('rows', [...inv.rows, { description: '', qty: 1, unit: 'ks', unitPrice: 0, total: 0, account: kind === 'purchase' ? '518001' : '602001' }]); const delRow = (i) => set('rows', inv.rows.length > 1 ? inv.rows.filter((_, j) => j !== i) : inv.rows); const total = inv.rows.reduce((a, r) => a + (Math.round((r.qty || 0) * (r.unitPrice || 0) * 100) / 100), 0); const save = async () => { if (!inv.partnerName) { setError(sales ? 'Vyber odberateľa.' : 'Vyber dodávateľa.'); return; } if (inv.rows.some(r => !r.description.trim())) { setError('Každá položka musí mať popis.'); return; } if (inv.dueDate < inv.issueDate) { setError('Splatnosť nemôže byť pred dátumom vystavenia.'); return; } setSaving(true); setError(null); try { const saved = isNew ? await uctStore.invoices.create(inv) : await uctStore.invoices.update(inv); onSaved(saved); } catch (e) { setError(e.message); } setSaving(false); }; return ( Dostane {sales ? 'číslo' : 'interné číslo'} {preview.number}{preview.vs && <> · VS {preview.vs}}{kind === 'proforma' && ' · zálohová faktúra nie je daňový doklad'} : <>VS {inv.vs}} onClose={onClose} footer={<> }> setError(null)} /> {sales && !company.name &&
Firma nemá vyplnené údaje - na faktúre bude prázdna hlavička. Doplň ich v Nastaveniach.
}
{!sales && (
set('supplierNumber', e.target.value)} placeholder="2026045" /> set('vs', e.target.value.replace(/\D/g, ''))} placeholder="2026045" />
)} {inv.partnerName && (
{inv.partnerName}
{inv.partnerAddress || bez adresy}
{[inv.partnerIco && `IČO ${inv.partnerIco}`, inv.partnerDic && `DIČ ${inv.partnerDic}`, inv.partnerIcDph && `IČ DPH ${inv.partnerIcDph}`].filter(Boolean).join(' · ')}
)}
{ const v = e.target.value; setInv(prev => ({ ...prev, issueDate: v, deliveryDate: prev.deliveryDate === prev.issueDate ? v : prev.deliveryDate, dueDate: isNew ? uctStore.util.addDays(v, uctStore.settings.get().defaultDueDays || 14) : prev.dueDate })); }} /> set('deliveryDate', e.target.value)} />
set('dueDate', e.target.value)} />
Položky
{showAccounts && } {inv.rows.map((r, i) => ( {showAccounts && } ))}
PopisMnožstvoMJCena za MJSpolu{sales ? 'Výnosový účet' : 'Nákladový účet'}
setRow(i, 'description', e.target.value)} /> setRow(i, 'qty', v)} /> setRow(i, 'unit', e.target.value)} /> setRow(i, 'unitPrice', v)} /> {fmt.eur(Math.round((r.qty || 0) * (r.unitPrice || 0) * 100) / 100)} setRow(i, 'account', v)} prefixes={sales ? ['6'] : ['5', '0', '1', '3']} />
{acc && kind !== 'proforma' && } {showAccounts && {sales ? 'Pohľadávka na účte' : 'Záväzok na účte'} set('account', v)} prefixes={[sales ? '31' : '32']} style={{display:'inline-flex', verticalAlign:'middle'}} className="input mono" />}
{company.vatPayer ? : }
Základ dane{fmt.eur(total)}
Neplatiteľ DPH
Celkom na úhradu{fmt.eur(total)}
{sales &&