// 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 => (
setFilterCat(filterCat === s.cat.id ? null : s.cat.id)}
>
{window.EXPENSE_TYPE_LABELS[s.cat.type].short}
{s.cat.label}
{s.monthly > 0
? <>{window.formatMoney(s.monthly)} ₴/міс >
: разові
}
{s.count} {window.plural(s.count, "позиція", "позиції", "позицій")}
))}
{/* Фільтр + таблиця */}
setFilterType("all")}>Усі
setFilterType("adm")}>АДМ
setFilterType("zvv")}>ЗВВ
setFilterVat("all")}>Усі
setFilterVat("vat")}>з ПДВ
setFilterVat("novat")}>без ПДВ
{filterCat && (
setFilterCat(null)}>
{window.getCategory(filterCat).label} ×
)}
Нова витрата
{/* Зведення ПДВ по показаних витратах */}
Сума з ПДВ
{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 (
<>
{window.EXPENSE_TYPE_LABELS[cat.type].label}
{expense.paid && сплачено }
{expense.title}
{cat.label} · {window.PERIODICITY_LABELS[expense.periodicity]}
Сума
Сума оплати {window.formatMoney(expense.amount)} ₴
У перерахунку на місяць {expense.monthlyAmount > 0 ? window.formatMoney(expense.monthlyAmount) + " ₴" : "—"}
Періодичність {window.PERIODICITY_LABELS[expense.periodicity]}
{expense.paid ? "Сплачено" : "Наступний платіж"} {window.formatDate(expense.paid ? expense.paidDate : expense.nextDate)}
ПДВ
{v.vatable ? (
<>
Ставка ПДВ {v.rate}%
Сума без ПДВ (нетто) {window.formatMoney(v.net)} ₴
ПДВ у сумі {window.formatMoney(v.vat)} ₴
Вхідний кредит {v.creditable ? window.formatMoney(v.creditable) + " ₴" : "—"}
{expense.taxInvoice && }
Податкова накладна зареєстрована
{expense.taxInvoice ? "Вхідний ПДВ іде в податковий кредит" : "Без накладної ПДВ не зменшує зобов'язання"}
>
) : (
Без ПДВ — {expense.note && /нерезидент|єдиному|звільнен|поза об/i.test(expense.note) ? expense.note.toLowerCase() : "постачальник не платник ПДВ або послуга звільнена"}. Вхідного кредиту немає.
)}
Постачальник
{expense.supplier}
{obj && (
Прив'язано до об'єкта
{obj.code} {obj.name}
{obj.client && window.getClient(window.DATA.CONTRACTS.find(c => c.obj === obj.id)?.client)?.short}
)}
{expense.note && (
)}
{!expense.paid && Позначити сплаченою }
Редагувати
>
);
}
// ============ КОШТОРИСИ ============
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)}%)
Експорт кошторису PDF
Прив'язати до договору
)}
);
}
window.ExpensesPage = ExpensesPage;
window.EstimatesPage = EstimatesPage;