// phase3-pages.jsx — P&L · Виплати підрядникам · Авансові звіти // ============ P&L ============ function PnlPage({ role }) { const contracts = window.DATA.CONTRACTS; const invoices = window.DATA.INVOICES; const expenses = window.DATA.EXPENSES; const team = window.DATA.TEAM; const subPayments = window.DATA.SUB_PAYMENTS; const [period, setPeriod] = React.useState("month"); // Спрощений розрахунок P&L const grossIncome = invoices.filter(i => i.status === "paid").reduce((s, i) => s + i.amount, 0); const subIncome = grossIncome * 0.84; // без ПДВ const pdvOut = grossIncome - subIncome; const monthlyExpenses = expenses.reduce((s, e) => s + (e.monthlyAmount || 0), 0); const salary = team.reduce((s, p) => { const pay = window.computePayroll(p.id, p.partRate ? 88 : 176, 176); return s + (pay?.gross || 0); }, 0); const subCosts = subPayments.filter(p => p.status === "paid").reduce((s, p) => s + p.amount, 0); const period_mult = period === "month" ? 1 : period === "quarter" ? 3 : 12; const revenue = subIncome; const directCosts = salary + subCosts * 0.2; // примірний розподіл const grossProfit = revenue - directCosts; const opex = monthlyExpenses * period_mult; const ebitda = grossProfit - opex; const taxes = ebitda > 0 ? ebitda * 0.05 : 0; const netProfit = ebitda - taxes; const margin = revenue > 0 ? (netProfit / revenue) * 100 : 0; return (

P&L · Звіт про прибутки і збитки

фінансовий результат компанії {period === "month" ? "травень 2026" : period === "quarter" ? "Q2 2026" : "2026 рік"}
Виторг
{window.formatMoney(Math.round(revenue))}
без ПДВ
Витрати разом
{window.formatMoney(Math.round(directCosts + opex))}
прямі + опер.
Чистий прибуток
= 0 ? "var(--c-green-deep)" : "var(--late)"}}> {netProfit >= 0 ? "+" : ""}{window.formatMoney(Math.round(netProfit))}
після податків
Маржа
{margin.toFixed(1)}%
net / revenue

Деталізація

за {period === "month" ? "травень 2026" : period === "quarter" ? "Q2 2026" : "2026 рік"}
Виторг (gross, з ПДВ) {window.formatMoney(grossIncome)} ₴
− ПДВ 20% −{window.formatMoney(Math.round(pdvOut))} ₴
Виторг (net, без ПДВ) {window.formatMoney(Math.round(revenue))} ₴
Оплата праці (gross) −{window.formatMoney(Math.round(salary))} ₴
Виплати підрядникам −{window.formatMoney(Math.round(subCosts * 0.2))} ₴
Валовий прибуток {window.formatMoney(Math.round(grossProfit))} ₴
Операційні витрати (АДМ/ЗВВ) −{window.formatMoney(Math.round(opex))} ₴
EBITDA {window.formatMoney(Math.round(ebitda))} ₴
Єдиний податок 5% −{window.formatMoney(Math.round(taxes))} ₴
Чистий прибуток {window.formatMoney(Math.round(netProfit))} ₴

Прибутковість по об'єктах

маржа по кожному договору
{contracts.map(c => { const obj = window.getObject(c.obj); const paid = c.schedule.filter(s => s.paidDate).reduce((sum, s) => sum + s.amount, 0); const costs = Math.round(c.total * 0.4); const profit = paid - costs; const pct = c.total > 0 ? (profit / c.total) * 100 : 0; return ( ); })}
Об'єкт Договір Сплачено Витрати Прибуток Маржа
{obj?.name}
{obj?.code}
{window.formatMoney(c.total)} ₴ {window.formatMoney(paid)} ₴ {window.formatMoney(costs)} ₴ = 0 ? "var(--c-green-deep)" : "var(--late)"}}> {profit >= 0 ? "+" : ""}{window.formatMoney(profit)} ₴
{pct.toFixed(0)}%
= 30 ? "var(--c-green-deep)" : pct >= 0 ? "var(--c-orange, var(--c-green))" : "var(--late)"}}>
); } // ============ ВИПЛАТИ ПІДРЯДНИКАМ ============ function SubPaymentsPage({ role }) { const payments = window.DATA.SUB_PAYMENTS; const [filter, setFilter] = React.useState("active"); const paid = payments.filter(p => p.status === "paid"); const issued = payments.filter(p => p.status === "issued"); const overdue = payments.filter(p => p.status === "overdue"); const totalPaid = paid.reduce((s, p) => s + p.amount, 0); const totalIssued = issued.reduce((s, p) => s + p.amount, 0); const totalOverdue = overdue.reduce((s, p) => s + p.amount, 0); let shown = payments; if (filter === "active") shown = [...overdue, ...issued]; else if (filter === "paid") shown = paid; else if (filter === "overdue") shown = overdue; return (

Виплати підрядникам

{payments.length} {window.plural(payments.length, "рахунок", "рахунки", "рахунків")} {overdue.length > 0 && <> {overdue.length} прострочено } виплачено за рік {window.formatMoney(totalPaid)} ₴
0 ? "is-warn" : ""}`}>
Прострочено
{window.formatMoney(totalOverdue)}
{overdue.length} рахунків
До сплати
{window.formatMoney(totalIssued)}
{issued.length} в графіку
Сплачено
{window.formatMoney(totalPaid)}
{paid.length} закритих
Активних
{issued.length + overdue.length}
потребують оплати
{shown.map(p => { const sup = window.DATA.SUPPLIERS.find(s => s.id === p.supplier); const obj = window.getObject(p.obj); const cType = window.CONTRACT_TYPE_LABELS[p.contractType]; return ( ); })}
№ рахунку Підрядник Об'єкт Тип Термін Сума Статус
{p.invoiceNumber}
{sup?.name}
{p.description.substring(0, 50)}{p.description.length > 50 ? "..." : ""}
{obj?.code} {cType.short} {p.pdvIncluded > 0 &&
ПДВ {window.formatMoney(p.pdvIncluded)} ₴
}
{window.formatDate(p.dueDate)}
{p.status === "overdue" &&
+{p.lateDays} дн.
} {p.status === "paid" && p.paidDate &&
сплачено {window.formatDate(p.paidDate)}
}
{window.formatMoney(p.amount)} ₴ {p.status === "paid" && сплачено} {p.status === "overdue" && прострочено} {p.status === "issued" && очікує}
); } // ============ АВАНСОВІ ЗВІТИ ============ function AdvancesPage({ role }) { const reports = window.DATA.ADVANCE_REPORTS; const [openId, setOpenId] = React.useState(null); const [filter, setFilter] = React.useState("pending"); const pending = reports.filter(r => r.status === "pending"); const approved = reports.filter(r => r.status === "approved"); const rejected = reports.filter(r => r.status === "rejected"); let shown = reports; if (filter === "pending") shown = pending; else if (filter === "approved") shown = approved; else if (filter === "rejected") shown = rejected; const totalApproved = approved.reduce((s, r) => s + r.amount, 0); const totalPending = pending.reduce((s, r) => s + r.amount, 0); return (

Авансові звіти

відрядження, дрібні витрати з власних коштів {pending.length > 0 && <> {pending.length} {window.plural(pending.length, "очікує", "очікують", "очікують")} затвердження }
0 ? "is-warn" : ""}`}>
На розгляді
{window.formatMoney(totalPending)}
{pending.length} {window.plural(pending.length, "звіт", "звіти", "звітів")}
Затверджено (рік)
{window.formatMoney(totalApproved)}
{approved.length} {window.plural(approved.length, "звіт", "звіти", "звітів")}
Відхилено
{rejected.length}
не відповідає політиці
{shown.map(r => setOpenId(r.id)} />)}
{openId && r.id === openId)} onClose={() => setOpenId(null)} />}
); } function AdvanceCard({ report, onOpen }) { const person = window.getTeam(report.who); const status = window.ADVANCE_STATUS[report.status]; return (
{status.label}
{person?.initials}
{report.title}
{person?.name} {window.formatDate(report.date)} {report.items.length} {window.plural(report.items.length, "позиція", "позиції", "позицій")}
сума
{window.formatMoney(report.amount)}
{report.status === "pending" ? чекає затвердження : report.status === "rejected" ? {report.rejectReason?.substring(0, 60)}… : повернено: {window.formatDate(report.approvedDate)} }
); } function AdvanceDrawer({ report, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const person = window.getTeam(report.who); const status = window.ADVANCE_STATUS[report.status]; return ( <>
); } window.PnlPage = PnlPage; window.SubPaymentsPage = SubPaymentsPage; window.AdvancesPage = AdvancesPage;