// salary-pages.jsx — Зарплата: Розрахунок · Реєстр виплат · Аналітика // ============ РОЗРАХУНОК ============ function PayrollPage({ role }) { const team = window.DATA.TEAM; const period = window.PAYROLL_PERIOD; window.useTabel(); // ререндер при зміні табеля const tabelSt = window.tabelStatus(period.id); const [editingId, setEditingId] = React.useState(null); const canEdit = role === "director" || role === "accountant"; const rows = team.map(p => { const hours = period.hoursByPerson[p.id] || 0; const plan = window.tabelNorm(period.id, p.id) || period.workhours; const pay = window.computePayroll(p.id, hours, plan); return { person: p, hours, pay }; }); const totalGross = rows.reduce((s, r) => s + (r.pay?.gross || 0), 0); const totalNet = rows.reduce((s, r) => s + (r.pay?.net || 0), 0); const totalTax = rows.reduce((s, r) => s + (r.pay?.pdfo + r.pay?.vz || 0), 0); const totalCash = rows.reduce((s, r) => s + (r.person.cashSalary || 0), 0); const showCash = window.useCashVisible() && window.canSeeCash(role); return (

Розрахунок ЗП

{period.label} {period.workdays} робочих днів · {period.workhours} год {period.status === "closed" ? Закрито {window.formatDate(period.closedAt)} : Період відкритий }
{tabelSt === "closed" ? <>Години взято із закритого табеля за {period.label}. Оклад штатних скориговано пропорційно нормі; лікарняні та відпустки — окремо. : <>Години — з табеля за {period.label}. Табель ще не закрито — суми попередні й оновляться після закриття місяця.} {window.__setPage && }
{/* Підсумок */}
До нарахування (gross)
{window.formatMoney(totalGross)}
за весь період
До виплати (net)
{window.formatMoney(totalNet)}
після утримань
Податки і збори
{window.formatMoney(totalTax)}
ПДФО 18% + ВЗ 5%
Працівників
{team.length}
{team.filter(t => t.employment === "staff").length} штат · {team.filter(t => t.employment === "contractor").length} ЦПХ
{showCash && Готівкою за період: {window.formatMoney(totalCash)} ₴ · до видачі разом {window.formatMoney(totalNet + totalCash)} ₴}
{showCash && } {showCash && } {rows.map(({ person, hours, pay }) => { const empl = window.EMPLOYMENT_LABELS[person.employment]; return ( {showCash && } {showCash && } ); })} {showCash && } {showCash && }
Працівник Модель Год Оклад Бонус ЄДЕССБ До нарах. ПДФО ВЗ ОфіційноГотівкоюДо видачі
{person.initials}
{person.name}
{person.role} · {empl?.short}
{window.SALARY_MODEL_LABELS[person.salaryModel]} {hours} {window.formatMoney(pay.base)} {pay.bonus > 0 ? window.formatMoney(pay.bonus) : } {pay.edessb > 0 ? {window.formatMoney(pay.edessb)} : } {window.formatMoney(pay.gross)} {pay.pdfo > 0 ? window.formatMoney(pay.pdfo) : "—"} {pay.vz > 0 ? window.formatMoney(pay.vz) : "—"} {window.formatMoney(pay.net)} ₴{person.cashSalary > 0 ? window.formatMoney(person.cashSalary) : }{window.formatMoney(pay.net + (person.cashSalary || 0))} ₴
Усього {window.formatMoney(rows.reduce((s,r) => s + (r.pay.edessb||0), 0))} {window.formatMoney(totalGross)} {window.formatMoney(rows.reduce((s,r) => s + r.pay.pdfo, 0))} {window.formatMoney(rows.reduce((s,r) => s + r.pay.vz, 0))} {window.formatMoney(totalNet)} ₴{window.formatMoney(totalCash)} ₴{window.formatMoney(totalNet + totalCash)} ₴
Бонуси розраховуються за правилами KPI · правила задаються РМ
{editingId && t.id === editingId)} hours={period.hoursByPerson[editingId] || 0} period={period} onClose={() => setEditingId(null)} />}
); } function PayrollDetail({ person, hours, period, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const plan = window.tabelNorm(period.id, person.id) || period.workhours; const pay = window.computePayroll(person.id, hours, plan); const empl = window.EMPLOYMENT_LABELS[person.employment]; return ( <>
); } // ============ РЕЄСТР ВИПЛАТ ============ function RegistryPage({ role }) { const team = window.DATA.TEAM; const period = window.PAYROLL_PERIOD; window.useTabel(); const rows = team.map(p => { const hours = period.hoursByPerson[p.id] || 0; const plan = window.tabelNorm(period.id, p.id) || period.workhours; const pay = window.computePayroll(p.id, hours, plan); return { person: p, pay, status: "ready" }; }); const total = rows.reduce((s, r) => s + r.pay.net, 0); const showCash = window.useCashVisible() && window.canSeeCash(role); const cashRows = team.filter(p => p.cashSalary > 0); const totalCash = cashRows.reduce((s, p) => s + p.cashSalary, 0); return (

Реєстр виплат

{period.label} сформовано {window.formatDate("2026-05-05")} Готово до банку
Сума на картки (офіційно, через банк)
{window.formatMoney(total)} ₴
{rows.length} платежів{showCash && totalCash > 0 ? <> · готівкою ще {window.formatMoney(totalCash)} ₴ : ""}
{rows.map((r, i) => ( ))}
Одержувач IBAN Призначення Сума Статус
{String(i+1).padStart(2,"0")}
{r.person.name}
{r.person.role}
{r.person.iban} {r.person.employment === "contractor" ? `Гонорар за роботу, ${period.label}, ЦПХ` : `Заробітна плата за ${period.label}`} {window.formatMoney(r.pay.net)} ₴ готово
Усього {rows.length} платежів {window.formatMoney(total)} ₴
{showCash && cashRows.length > 0 && (

Готівкою · в конверті

поза банківським реєстром · конфіденційно
{cashRows.map((p, i) => ( ))}
Одержувач Призначення Сума Спосіб
{String(i+1).padStart(2,"0")}
{p.name}
{p.role}
Доплата готівкою, {period.label} {window.formatMoney(p.cashSalary)} ₴ готівка
Усього готівкою {window.formatMoney(totalCash)} ₴
Управлінський облік. Ці виплати не проходять через банк і офіційну звітність. Розділ бачать лише директор і бухгалтер. Майте на увазі податкові та юридичні ризики неофіційних виплат.
)}
); } // ============ АНАЛІТИКА ============ function AnalyticsPage({ role }) { const team = window.DATA.TEAM; const objects = window.DATA.OBJECTS; // Фонд ЗП по місяцях (моки) const fundMonths = [ { m: "Грудень 25", v: 234000 }, { m: "Січень 26", v: 241500 }, { m: "Лютий 26", v: 238900 }, { m: "Березень 26",v: 252100 }, { m: "Квітень 26", v: 247600 }, { m: "Травень 26 (план)", v: 251200, projected: true }, ]; const maxFund = Math.max(...fundMonths.map(x => x.v)); // Розподіл фонду по типах const breakdown = [ { label: "Штатні оклади", v: 174000, color: "var(--c-green-deep)" }, { label: "Бонуси за проєкти", v: 38000, color: "var(--c-green)" }, { label: "Часткова зайнятість", v: 22000, color: "var(--c-blue)" }, { label: "Субпідряд (ЦПХ)", v: 13600, color: "var(--c-mint)" }, ]; const breakSum = breakdown.reduce((s, x) => s + x.v, 0); // Дохід vs ЗП по об'єктах const objAnalytics = objects.map(o => { const contractM = parseFloat(o.contract.replace(/[^\d.]/g, "")); const contract = contractM * 1000000; // спрощено: ЗП = 25% від контракту const salary = Math.round(contract * 0.25); const margin = contract - salary; return { obj: o, contract, salary, margin }; }); return (

Аналітика ЗП

фонд оплати праці навантаження команди прибутковість по об'єктах
{/* Фонд по місяцях */}

Фонд ЗП по місяцях

останні 6 місяців
{fundMonths.map(m => (
{Math.round(m.v / 1000)}k
{m.m}
))}
{/* Розподіл */}

Структура фонду · травень

{window.formatMoney(breakSum)} ₴ загалом
{breakdown.map(b => (
))}
{breakdown.map(b => (
{b.label} {window.formatMoney(b.v)} ₴ {Math.round(b.v / breakSum * 100)}%
))}
{/* Завантаження команди */}

Завантаження команди

розподіл по людях
{team.map(p => { const eff = Math.round(80 + Math.random() * 25 - (p.load > 90 ? 15 : 0)); return ( ); })}
Працівник Завантаження Активних задач Ефективність
{p.initials}
{p.name}
= 90 ? "is-high" : ""}`} style={{width: p.load + "%"}}>
{p.load}%
{eff}%
{/* Прибутковість */}

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

оцінка · ЗП склад ~25% контракту
{objAnalytics.map(({ obj, contract, salary, margin }) => { const marginPct = Math.round(margin / contract * 100); return ( ); })}
Шифр Об'єкт Договір Витрати на ЗП Маржа Маржа
{obj.code}
{obj.name}
{window.formatMoney(Math.round(contract))} ₴ {window.formatMoney(salary)} ₴ {window.formatMoney(margin)} ₴
{marginPct}%
); } window.PayrollPage = PayrollPage; window.RegistryPage = RegistryPage; window.AnalyticsPage = AnalyticsPage;