// expense-pages.jsx — Витрати компанії · Кошториси на проєктування // ============ ВИТРАТИ ============ function ExpensesPage({ role }) { const expenses = window.DATA.EXPENSES; const cats = window.EXPENSE_CATEGORIES; const [filterType, setFilterType] = React.useState("all"); const [filterCat, setFilterCat] = React.useState(null); const [filterVat, setFilterVat] = React.useState("all"); // all | vat | novat const [openId, setOpenId] = React.useState(null); const [, setTick] = React.useState(0); // Розрахунок місячних сум по типах const totalAdm = window.totalMonthlyByType("adm"); const totalZvv = window.totalMonthlyByType("zvv"); const totalMonth = totalAdm + totalZvv; const totalYear = totalMonth * 12; // Фільтрація let shown = expenses; if (filterType !== "all") { shown = shown.filter(e => window.getCategory(e.cat).type === filterType); } if (filterCat) { shown = shown.filter(e => e.cat === filterCat); } if (filterVat !== "all") { shown = shown.filter(e => (window.expenseVat(e).vatable ? "vat" : "novat") === filterVat); } // Сортування: щомісячні зверху, потім за сумою const periodOrder = { monthly: 0, quarterly: 1, yearly: 2, once: 3 }; shown = [...shown].sort((a,b) => { const p = periodOrder[a.periodicity] - periodOrder[b.periodicity]; if (p !== 0) return p; return (b.monthlyAmount || b.amount) - (a.monthlyAmount || a.amount); }); // Підсумок ПДВ по показаних витратах (брутто → нетто, вхідний кредит) const vatSum = shown.reduce((a, e) => { const v = window.expenseVat(e); a.gross += v.gross; a.net += v.net; a.vat += v.vat; if (v.creditable) a.credit += v.creditable; else if (v.vatable) a.noInvoice += v.vat; return a; }, { gross: 0, net: 0, vat: 0, credit: 0, noInvoice: 0 }); // Категорії з підрахунком const catStats = cats.map(c => { const list = expenses.filter(e => e.cat === c.id); const monthly = list.reduce((s, e) => s + (e.monthlyAmount || 0), 0); return { cat: c, count: list.length, monthly }; }).filter(s => s.count > 0); return (

Витрати компанії

{expenses.length} {window.plural(expenses.length, "стаття витрат", "статті витрат", "статей витрат")} місячний бюджет {window.formatMoney(totalMonth)} ₴ річний — {window.formatMoney(totalYear)} ₴
Адміністративні (АДМ)
{window.formatMoney(totalAdm)} ₴/міс
{Math.round(totalAdm / totalMonth * 100)}% бюджету
Загальновиробничі (ЗВВ)
{window.formatMoney(totalZvv)} ₴/міс
{Math.round(totalZvv / totalMonth * 100)}% бюджету
Всього на місяць
{window.formatMoney(totalMonth)}
{window.formatMoney(Math.round(totalMonth / 22))} ₴/день
Платіжні зобов'язання
{expenses.filter(e => e.periodicity === "monthly" && !e.paid).length}
найближчі цього місяця
{/* Структура витрат */}

Структура місячних витрат

за категоріями
{catStats.filter(s => s.monthly > 0).map(s => (
setFilterCat(filterCat === s.cat.id ? null : s.cat.id)}>
))}
{catStats.map(s => ( ))}
{/* Фільтр + таблиця */}
{filterCat && ( )}
{/* Зведення ПДВ по показаних витратах */}
Сума з ПДВ
{window.formatMoney(vatSum.gross)} ₴
Без ПДВ (нетто)
{window.formatMoney(vatSum.net)} ₴
Вхідний ПДВ (кредит)
{window.formatMoney(vatSum.credit)} ₴
з податковою накладною
{vatSum.noInvoice > 0 && (
ПДВ без накладної
{window.formatMoney(vatSum.noInvoice)} ₴
не йде в кредит
)}
{shown.map(e => { const cat = window.getCategory(e.cat); const obj = e.obj ? window.getObject(e.obj) : null; const due = window.daysBetween(window.TODAY.day === "26" ? "2026-05-26" : "2026-05-26", e.nextDate); return ( setOpenId(e.id)}> ); })}
Витрата Тип Постачальник Періодичність Сума ПДВ У місяць Наступний платіж
{e.title}
{cat.label}{obj && <> · {obj.code}}
{window.EXPENSE_TYPE_LABELS[cat.type].short} {e.supplier} {window.PERIODICITY_LABELS[e.periodicity]} {window.formatMoney(e.amount)} ₴ {(() => { const v = window.expenseVat(e); if (!v.vatable) return без ПДВ; return (
{window.formatMoney(v.vat)} ₴ {v.rate}% {v.hasInvoice ? "· ПН ✓" : "· без ПН"}
); })()}
{e.monthlyAmount > 0 ? {window.formatMoney(e.monthlyAmount)} ₴ : } {e.paid ? сплачено {window.formatDate(e.paidDate)} : {window.formatDate(e.nextDate)} }
{openId && e.id === openId)} onClose={() => setOpenId(null)} onChange={() => setTick(x => x + 1)} />}
); } function catColor(cat) { const adm = ["#068b49", "#3c6287", "#8ec975", "#98dab4", "#5a8aa8", "#7fa8c4", "#a8c8d8", "#b8d5b8"]; const zvv = ["#e37601", "#d4a373", "#c89668", "#b88058", "#a87048", "#986038"]; const all = window.EXPENSE_CATEGORIES; const idx = all.findIndex(c => c.id === cat.id); return cat.type === "adm" ? adm[idx % adm.length] : zvv[idx % zvv.length]; } function ExpenseDrawer({ expense, onClose, onChange }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const cat = window.getCategory(expense.cat); const obj = expense.obj ? window.getObject(expense.obj) : null; const v = window.expenseVat(expense); const toggleInvoice = () => { expense.taxInvoice = !expense.taxInvoice; onChange && onChange(); }; return ( <>
); } // ============ КОШТОРИСИ ============ function EstimatesPage({ role }) { const objects = window.DATA.OBJECTS; const contracts = window.DATA.CONTRACTS.filter(c => c.status === "active" || c.status === "supervision"); const allocations = window.allocateOverheadToObjects(); const totalAdmMo = window.totalMonthlyByType("adm"); const totalZvvMo = window.totalMonthlyByType("zvv"); return (

Кошториси на проєктування

розподіл АДМ і ЗВВ між об'єктами методика: пропорційно сумі договору
{/* Налаштування */}

Нормативні коефіцієнти

застосовуються до кошторисів
АДМ-витрати
{window.OVERHEAD_RATES.admPct}%
від ОП виконавців
{window.formatMoney(totalAdmMo)} ₴/міс факт
ЗВВ-витрати
{window.OVERHEAD_RATES.zvvPct}%
від ОП виконавців
{window.formatMoney(totalZvvMo)} ₴/міс факт
Прибуток
{window.OVERHEAD_RATES.profitPct}%
плановий норматив
Метод розподілу
% від договору
пропорційно
Як це працює: на кожен об'єкт автоматично нараховується частка АДМ і ЗВВ-витрат — пропорційно сумі договору. Це включається у вартість проєктування (Настанова ДБН Д.1.1-7). У кошторисі бачите: пряму ОП → надбавку АДМ → надбавку ЗВВ → прибуток → фінальна вартість.
{/* Розрахунок по кожному об'єкту */}

Розподіл по активних об'єктах

{allocations.length} об'єктів · щомісяця
{allocations.map(a => ( ))}
{/* Підсумок */}

Перевірка

вся сума АДМ і ЗВВ має бути розподілена
АДМ Розподілено по об'єктах {window.formatMoney(allocations.reduce((s,a) => s + a.monthlyAdm, 0))} ₴ з {window.formatMoney(totalAdmMo)} ₴ факту
ЗВВ Розподілено по об'єктах {window.formatMoney(allocations.reduce((s,a) => s + a.monthlyZvv, 0))} ₴ з {window.formatMoney(totalZvvMo)} ₴ факту
Всього {window.formatMoney(allocations.reduce((s,a) => s + a.monthlyTotal, 0))} ₴ усі витрати на діючі об'єкти
); } function EstimateRow({ allocation }) { const { contract, shareRatio, monthlyAdm, monthlyZvv, monthlyTotal } = allocation; const [open, setOpen] = React.useState(false); const obj = window.getObject(contract.obj); const client = window.getClient(contract.client); const pct = (shareRatio * 100).toFixed(1); // Спрощено: оцінка ОП виконавців (25% від контракту як базова цифра) const estimatedLabor = Math.round(contract.total * 0.25); const admAddition = Math.round(estimatedLabor * window.OVERHEAD_RATES.admPct / 100); const zvvAddition = Math.round(estimatedLabor * window.OVERHEAD_RATES.zvvPct / 100); const totalCost = estimatedLabor + admAddition + zvvAddition; const profit = Math.round(totalCost * window.OVERHEAD_RATES.profitPct / 100); const finalEstimate = totalCost + profit; const margin = contract.total - finalEstimate; const marginPct = margin / contract.total * 100; return (
setOpen(!open)}>
{obj?.code}
{obj?.name}
{client?.short}
Частка
{pct}%
від загального обсягу
АДМ {window.formatMoney(monthlyAdm)} ₴ /міс
ЗВВ {window.formatMoney(monthlyZvv)} ₴ /міс
Кошторис на проєктування
{window.formatMoney(finalEstimate)}
= 15 ? "is-good" : marginPct >= 0 ? "is-neutral" : "is-bad"}`}> {margin >= 0 ? "+" : ""}{window.formatMoney(margin)} ₴ ({marginPct.toFixed(1)}%) маржа
{open && (
Оплата праці виконавців (ОП) оцінка: 25% від договору · з табелю годин {window.formatMoney(estimatedLabor)} ₴
АДМ {window.OVERHEAD_RATES.admPct}% × ОП оренда, ПЗ, зв'язок, реклама + {window.formatMoney(admAddition)} ₴
ЗВВ {window.OVERHEAD_RATES.zvvPct}% × ОП субпідряд, експертиза, виїзди + {window.formatMoney(zvvAddition)} ₴
Прямі + накладні {window.formatMoney(totalCost)} ₴
Прибуток {window.OVERHEAD_RATES.profitPct}% плановий норматив + {window.formatMoney(profit)} ₴
Кошторисна вартість {window.formatMoney(finalEstimate)} ₴
Сума договору що фактично домовлено {window.formatMoney(contract.total)} ₴
= 15 ? "is-good" : marginPct >= 0 ? "is-neutral" : "is-bad"}`}> {margin >= 0 ? "Маржа" : "Збиток"} різниця між договором і вартістю {margin >= 0 ? "+" : ""}{window.formatMoney(margin)} ₴ ({marginPct.toFixed(1)}%)
)}
); } window.ExpensesPage = ExpensesPage; window.EstimatesPage = EstimatesPage;