// cashflow-page.jsx — Cash flow: баланс, надходження, виплати, прогноз function CashflowPage({ role }) { const accounts = window.DATA.CASH_ACCOUNTS; const ops = window.DATA.CASH_OPS; const invoices = window.DATA.INVOICES; const expenses = window.DATA.EXPENSES; const today = "2026-05-26"; const [horizon, setHorizon] = React.useState(12); // тижнів вперед // ===== Поточні баланси ===== const usdRate = 41.5; const totalUah = accounts.reduce((s, a) => s + (a.currency === "UAH" ? a.balance : a.balance * usdRate), 0); const mainBalance = accounts.find(a => a.primary)?.balance || 0; // ===== Прогноз: побудова тижневих кошиків ===== const weeks = buildWeeks(today, horizon); const monthlyOutflow = expenses.reduce((s, e) => s + (e.monthlyAmount || 0), 0); const monthlySalary = window.DATA.TEAM.reduce((s, p) => { const pay = window.computePayroll(p.id, p.partRate ? 88 : 176, 176); return s + (pay?.net || 0); }, 0); // Надходження: усі неоплачені інвойси з dueDate у горизонті const upcomingIn = invoices.filter(i => i.status !== "paid" && i.dueDate >= today); // Виплати: щомісячні витрати (розкидані по тижнях рівномірно) + ЗП в кінці кожного місяця // Для прогнозу спрощуємо: за тиждень = monthlyOutflow / 4.33 для нормальних витрат const weeklyExpense = monthlyOutflow / 4.33; // Заповнюємо тижні let runningBalance = totalUah; weeks.forEach(w => { // надходження upcomingIn.forEach(inv => { if (inv.dueDate >= w.start && inv.dueDate <= w.end) { w.inflow += inv.amount; w.inflowItems.push({ kind: "invoice", inv }); } }); // витрати: розкидаємо щомісячні w.outflow += weeklyExpense; // ЗП в останньому тижні кожного місяця (приблизно) const monthEnd = new Date(w.end); const nextDay = new Date(monthEnd); nextDay.setDate(nextDay.getDate() + 1); if (nextDay.getMonth() !== monthEnd.getMonth()) { w.outflow += monthlySalary; w.outflowItems.push({ kind: "salary", amount: monthlySalary }); } // Великі субпідряд/експертиза витрати з nextDate в цьому тижні expenses.forEach(e => { if (e.periodicity === "once" && !e.paid && e.nextDate >= w.start && e.nextDate <= w.end) { w.outflow += e.amount; w.outflowItems.push({ kind: "expense", e }); } }); w.net = w.inflow - w.outflow; runningBalance += w.net; w.balance = runningBalance; if (runningBalance < 0) w.hasGap = true; }); const minBalance = Math.min(...weeks.map(w => w.balance), totalUah); const gapWeeks = weeks.filter(w => w.hasGap); const totalInflow = weeks.reduce((s, w) => s + w.inflow, 0); const totalOutflow = weeks.reduce((s, w) => s + w.outflow, 0); return (
| Тиждень | Надходження | Виплати | Чистий рух | Баланс на кінець | Деталі |
|---|---|---|---|---|---|
|
{w.label}
{w.range}
|
0 ? "var(--c-green-deep)" : "var(--ink-4)"}}> {w.inflow > 0 ? "+" + window.formatMoney(w.inflow) + " ₴" : "—"} | −{window.formatMoney(Math.round(w.outflow))} ₴ | = 0 ? "var(--c-green-deep)" : "var(--late)"}}> {w.net >= 0 ? "+" : ""}{window.formatMoney(Math.round(w.net))} ₴ | {window.formatMoney(Math.round(w.balance))} ₴ | {w.inflowItems.length + w.outflowItems.length > 0 ? : тільки регулярні } |
| Дата | Опис | Рахунок | Сума |
|---|---|---|---|
| {window.formatDate(o.date)} |
{o.type === "in" ? "↓" : "↑"}
{o.note}
|
{acc?.label} | 0 ? "var(--c-green-deep)" : "var(--ink)"}}> {o.amount > 0 ? "+" : ""}{window.formatMoney(o.amount)} ₴ |