// cash-register.jsx — Каса (неофіційний готівковий контур) // ⚠ Поза офіційним обліком. Доступ лише director / accountant + перемикач видимості готівки. // Фіксує готівкові надходження (підрядники/замовники) і видатки (ЗП «у конверті», постачальники). function CashKindChip({ kind }) { const k = (window.CASH_LEDGER_KINDS || {})[kind] || { label: kind, dir: "both" }; return {k.label}; } function CashAddForm({ onAdd, onCancel }) { const KINDS = window.CASH_LEDGER_KINDS; const [dir, setDir] = React.useState("in"); const [kind, setKind] = React.useState("client"); const [counterparty, setCounterparty] = React.useState(""); const [amount, setAmount] = React.useState(""); const [date, setDate] = React.useState(window.SYS_DATE); const [note, setNote] = React.useState(""); const kindOpts = Object.entries(KINDS).filter(([, v]) => v.dir === dir || v.dir === "both"); React.useEffect(() => { if (!kindOpts.some(([id]) => id === kind)) setKind(kindOpts[0][0]); }, [dir]); const num = parseFloat(String(amount).replace(/\s/g, "").replace(",", ".")) || 0; const canSave = counterparty.trim() && num > 0; const save = () => { if (!canSave) return; onAdd({ id: "c-" + Math.random().toString(36).slice(2, 8), date, dir, kind, amount: Math.round(num), counterparty: counterparty.trim(), note: note.trim() }); }; return (
setCounterparty(e.target.value)} placeholder="Напр.: Замовник котеджу · Лісники" />
setAmount(e.target.value)} placeholder="напр.: 40000" inputMode="numeric" />
setDate(e.target.value)} />
setNote(e.target.value)} placeholder="деталі (необов'язково)" />
); } function CashRegisterPage({ role }) { const cashVisible = window.useCashVisible(); const L = window.DATA.CASH_LEDGER; const [ops, setOps] = React.useState(L.ops); const [filter, setFilter] = React.useState("all"); // all | in | out const [adding, setAdding] = React.useState(false); // 1) Доступ за роллю if (!window.canSeeCash(role)) { return (

Доступ обмежено

Готівковий контур бачать лише директор і бухгалтер.

); } // Хронологічний баланс const chrono = [...ops].sort((a, b) => a.date.localeCompare(b.date)); let run = L.opening; const balanceById = {}; chrono.forEach(o => { run += (o.dir === "in" ? o.amount : -o.amount); balanceById[o.id] = run; }); const balance = run; const totalIn = ops.filter(o => o.dir === "in").reduce((s, o) => s + o.amount, 0); const totalOut = ops.filter(o => o.dir === "out").reduce((s, o) => s + o.amount, 0); const salaryOut = ops.filter(o => o.dir === "out" && o.kind === "salary").reduce((s, o) => s + o.amount, 0); const cashSalaryMonthly = window.totalCashMonthly(); // Розбивка за призначенням const byKind = {}; ops.forEach(o => { byKind[o.kind] = (byKind[o.kind] || 0) + o.amount; }); const addOp = (op) => { const next = [op, ...ops]; setOps(next); window.DATA.CASH_LEDGER.ops = next; setAdding(false); }; const removeOp = (id) => { const next = ops.filter(o => o.id !== id); setOps(next); window.DATA.CASH_LEDGER.ops = next; }; const paySalaryEnvelopes = () => { addOp({ id: "c-" + Math.random().toString(36).slice(2, 8), date: window.SYS_DATE, dir: "out", kind: "salary", amount: cashSalaryMonthly, counterparty: "ЗП «у конверті» · поточний місяць", note: window.DATA.TEAM.filter(p => p.cashSalary > 0).map(p => `${p.name.split(" ")[1] || p.name} ${window.formatMoney(p.cashSalary)}`).join(" · "), }); }; const shown = filter === "all" ? ops : ops.filter(o => o.dir === filter); const shownSorted = [...shown].sort((a, b) => b.date.localeCompare(a.date) || (b.id > a.id ? 1 : -1)); // 2) Гейт видимості готівки if (!cashVisible) { return (

Каса

готівковий контур · конфіденційно

Готівку приховано

Це неофіційний управлінський облік готівки. Дані чутливі — увімкніть показ готівки, щоб переглянути касу.

); } return (

Каса

готівковий контур {ops.length} операцій з {window.formatDate(L.openingDate)}
{/* Банер конфіденційності */}
Неофіційний управлінський облік. Поза бухгалтерією та банком. Частина підрядників і замовників розраховується готівкою; з цих коштів виплачується готівкова частина ЗП «у конверті» та дрібні витрати. Доступ — лише директор і бухгалтер.
{/* KPI */}
Залишок готівки
{window.formatMoney(balance)}
на {window.formatDate(window.SYS_DATE)}
Надходження
+{window.formatMoney(totalIn)}
за весь облік
Видатки
−{window.formatMoney(totalOut)}
за весь облік
З них на ЗП «у конверті»
{window.formatMoney(salaryOut)}
{window.formatMoney(cashSalaryMonthly)} ₴/міс за моделлю
{/* Розбивка за призначенням */}

Рух за призначенням

надходження та видатки
{Object.entries(byKind).map(([kind, sum]) => { const k = window.CASH_LEDGER_KINDS[kind] || {}; return (
{k.label || kind}
{k.dir === "in" ? "+" : "−"}{window.formatMoney(sum)} ₴
); })}
{/* Тулбар */}
{adding && setAdding(false)} />} {/* Журнал */} {shownSorted.map(o => ( ))}
Дата Призначення Контрагент / опис Сума Залишок
{window.formatDate(o.date)}
{o.counterparty}
{o.note &&
{o.note}
}
{o.dir === "in" ? "+" : "−"}{window.formatMoney(o.amount)} ₴ {window.formatMoney(balanceById[o.id])} ₴
Початковий залишок на {window.formatDate(L.openingDate)} {window.formatMoney(L.opening)} ₴ {window.formatMoney(balance)} ₴
Навіщо це. Готівка від підрядників і замовників не проходить через банк, але реальні гроші є — і з них ідуть готівкові виплати. Каса показує чесний залишок і рух, щоб кошти не «губилися». У офіційну звітність (P&L, банк, ДПС) ці суми не потрапляють.
); } window.CashRegisterPage = CashRegisterPage;