// contracts-billing.jsx — Графік фінансування договорів: життєвий цикл етапу, // дії на рядку (рахунок / акт / оплата), реєстр додаткових угод. // // Правило каналу оплати (за реквізитами замовника): // • казна (держ. замовник) → ЛИШЕ акт (рахунок не виставляємо) // • банк (госпрозрахунок) → рахунок + акт; аванс на банк — лише рахунок // // Етап графіку (schedule item) збагачено полями: // billing: "advance" | "work" — аванс (без акта) / за виконані роботи (з актом) // edessb: bool — етап пов'язаний з ЄДЕССБ / експертизою // dueDate (план) / factDate — планова vs фактична дата готовності етапу // invoiceNo / invoiceDate — № і дата виставленого рахунку // actNo / actDate — № і дата підписаного акта // paidDate — фактична дата оплати // ready: bool — готовність етапу (роботи виконані на об'єкті) (function () { const TODAY = window.TODAY_ISO || "2026-05-26"; // ── Канал оплати ─────────────────────────────────────────── // Держ. замовник (type:"state") платить через Держказну → лише акт. window.billingChannel = function (contract) { const client = window.getClient(contract.client); return client && client.type === "state" ? "treasury" : "bank"; }; window.BILLING_CHANNEL = { treasury: { label: "Казна — лише акт", short: "Казна", icon: "landmark", tone: "neutral", note: "Держ. замовник: оплата через Держказначейство за підписаним актом. Рахунок не виставляється." }, bank: { label: "Банк — рахунок + акт", short: "Банк", icon: "banknote", tone: "live", note: "Госпрозрахунковий замовник: спершу рахунок, акт за виконаними роботами, далі оплата." }, }; // ── Роль сторін у договорі (відома на етапі укладення) ────── // Наш бік: Виконавець (договір про надання послуг) або Підрядник (договір підряду). // Контрагент: Замовник або Генпідрядник (коли ми субпідрядник). // Предмет акта (надані послуги / виконані роботи) — теж залежить від договору. window.ACT_OUR_ROLES = { executor: { label: "ВИКОНАВЕЦЬ", labelGen: "Виконавця", short: "Виконавець" }, contractor: { label: "ПІДРЯДНИК", labelGen: "Підрядника", short: "Підрядник" }, }; window.ACT_COUNTER_ROLES = { customer: { label: "ЗАМОВНИК", labelGen: "Замовника", short: "Замовник" }, genContractor: { label: "ГЕНПІДРЯДНИК", labelGen: "Генпідрядника", short: "Генпідрядник" }, }; window.ACT_SUBJECTS = { services: { titleSuffix: "наданих послуг", verb: "надав", verbPl: "надано", noun: "послуги", genPl: "послуг", unit: "послуга", noteDone: "Послуги надані в повному обсязі" }, works: { titleSuffix: "виконаних робіт", verb: "виконав", verbPl: "виконано", noun: "роботи", genPl: "робіт", unit: "робота", noteDone: "Роботи виконані в повному обсязі" }, }; // Нормалізовані ролі договору (з дефолтами) window.contractParties = function (contract) { const p = (contract && contract.parties) || {}; const ourRole = p.ourRole === "contractor" ? "contractor" : "executor"; // Виконавець завжди працює із Замовником; Генпідрядник можливий лише як субпідрядник const counterRole = ourRole === "executor" ? "customer" : (p.counterRole === "genContractor" ? "genContractor" : "customer"); const subject = p.subject === "works" || p.subject === "services" ? p.subject : (ourRole === "contractor" ? "works" : "services"); return { ourRole, counterRole, subject }; }; // Повний конфіг формулювань для акта window.actRoleConfig = function (contract) { const { ourRole, counterRole, subject } = window.contractParties(contract); return { ourRole, counterRole, subject, our: window.ACT_OUR_ROLES[ourRole], counter: window.ACT_COUNTER_ROLES[counterRole], subj: window.ACT_SUBJECTS[subject], }; }; // ── Які документи потрібні для етапу ─────────────────────── window.stageDocs = function (contract, stage) { const channel = window.billingChannel(contract); const isAdvance = stage.billing === "advance"; return { channel, needsInvoice: channel === "bank", // казна — без рахунку needsAct: channel === "treasury" || !isAdvance, // казна завжди; банк — лише «за роботи» }; }; // ── Статус етапу (обчислюється з наявних документів) ─────── window.STAGE_STATUS = { planned: { label: "Заплановано", tone: "neutral", icon: "circle" }, invoiced: { label: "Рахунок виставлено", tone: "blue", icon: "file" }, acted: { label: "Акт підписано", tone: "amber", icon: "checkCircle" }, paid: { label: "Оплачено", tone: "live", icon: "check" }, overdue: { label: "Прострочено", tone: "warn", icon: "alert" }, }; window.stageStatus = function (contract, stage) { const d = window.stageDocs(contract, stage); if (stage.paidDate) return "paid"; let s = "planned"; if (d.needsInvoice && stage.invoiceNo) s = "invoiced"; if (d.needsAct && stage.actDate) s = "acted"; // прострочення — лише поки не оплачено й термін минув if (s === "planned" && stage.dueDate && stage.dueDate < TODAY) return "overdue"; return s; }; // ── Доступні дії на рядку ────────────────────────────────── // Повертає масив { key, label, icon, kind } у логічному порядку. window.stageActions = function (contract, stage) { const d = window.stageDocs(contract, stage); const acts = []; if (stage.paidDate) return acts; // закрито if (d.needsInvoice && !stage.invoiceNo) acts.push({ key: "invoice", label: "Виставити рахунок", icon: "file", kind: "default" }); if (d.needsAct && !stage.actDate) acts.push({ key: "act", label: "Сформувати акт", icon: "clipboard", kind: "default" }); // оплату можна позначити, коли всі потрібні документи готові const invoiceReady = !d.needsInvoice || stage.invoiceNo; const actReady = !d.needsAct || stage.actDate; if (invoiceReady && actReady) acts.push({ key: "pay", label: "Позначити оплаченим", icon: "check", kind: "primary" }); return acts; }; // ── Зведення по договору (для шапки) ─────────────────────── window.contractBillingSummary = function (contract) { const sched = contract.schedule || []; let paid = 0, invoiced = 0, acted = 0, overdue = 0, planned = 0; sched.forEach(s => { const st = window.stageStatus(contract, s); if (st === "paid") paid += s.amount; else if (st === "overdue") overdue += s.amount; else if (st === "acted") acted += s.amount; else if (st === "invoiced") invoiced += s.amount; else planned += s.amount; }); const billed = invoiced + acted; // у роботі, ще не оплачено return { paid, billed, overdue, planned, acted, invoiced }; }; // ═══════════════ ЗБАГАЧЕННЯ НАЯВНИХ ДАНИХ ═══════════════ // Інференс типу етапу за назвою (аванс / роботи) + ЄДЕССБ-маркер. function inferBilling(stageName) { return /аванс/i.test(stageName) ? "advance" : "work"; } function inferEdessb(stageName) { return /єдессб|експертиз/i.test(stageName); } // Підтягнути № і дату рахунку з вже існуючого реєстру INVOICES. function invoiceMeta(invId) { const inv = (window.DATA.INVOICES || []).find(i => i.id === invId); return inv ? { invoiceNo: inv.number, invoiceDate: inv.issueDate } : {}; } function enrichSchedule() { (window.DATA.CONTRACTS || []).forEach(c => { const channel = window.billingChannel(c); (c.schedule || []).forEach((s, idx) => { if (s.billing == null) s.billing = inferBilling(s.stage); if (s.edessb == null) s.edessb = inferEdessb(s.stage); // рахунок: з реєстру INVOICES (лише для банківського каналу) if (channel === "bank" && s.invoice && s.invoiceNo == null) { Object.assign(s, invoiceMeta(s.invoice)); } // акт: для оплачених «робочих» етапів вважаємо акт підписаним const needsAct = channel === "treasury" || s.billing !== "advance"; if (needsAct && s.paidDate && s.actNo == null) { const obj = window.getObject(c.obj); const codeNum = obj && obj.code ? (obj.code.match(/(\d+)\s*$/) || [])[1] : (c.number || "").split("/")[0]; s.actNo = `${codeNum}-АКТ-${String(idx + 1).padStart(2, "0")}`; // акт підписують орієнтовно за 3 дні до оплати const d = new Date(s.paidDate); d.setDate(d.getDate() - 3); s.actDate = d.toISOString().slice(0, 10); } // фактична дата готовності етапу if (s.factDate == null) { if (s.paidDate) s.factDate = s.actDate || s.dueDate; else s.factDate = null; } if (s.ready == null) s.ready = !!s.paidDate; }); }); } // ── Реєстр додаткових угод (приклади на наявних договорах) ── function installAmendments() { const map = { "k-47": [ { no: "1", date: "2025-12-20", reason: "Зміна графіку фінансування", changes: [ "Додано етап «Реєстрація в ЄДЕССБ» (10%, 840 000 ₴)", "Термін «Здача робочого проєкту»: 30.04 → 12.06.2026", ] }, ], "k-32": [ { no: "1", date: "2026-01-15", reason: "Перенесення термінів за результатами тендеру", changes: [ "Аванс після тендеру: 25% → 30% (930 000 ₴)", "Термін «Експертиза кошторису»: 31.03 → 30.04.2026", ] }, { no: "2", date: "2026-04-05", reason: "Збільшення обсягу — додатковий корпус", changes: [ "Загальна сума договору: 2 850 000 → 3 100 000 ₴", "Перерозподіл часток етапів робочого проєкту", ] }, ], "k-89": [ { no: "1", date: "2025-07-01", reason: "Продовження на авторський нагляд", changes: [ "Додано етапи авторського нагляду (2×5%, разом 80 000 ₴)", "Статус договору: активний → авт. нагляд", ] }, ], }; (window.DATA.CONTRACTS || []).forEach(c => { if (c.amendments == null) c.amendments = map[c.id] || []; // Ролі сторін: усі наявні договори — ми Виконавець (за домовленістю) if (c.parties == null) c.parties = { ourRole: "executor", counterRole: "customer", subject: "services" }; }); } enrichSchedule(); installAmendments(); window.enrichSchedule = enrichSchedule; // ═══════════════ КОМПОНЕНТ ГРАФІКА ═══════════════ const { useState } = React; const Ico = window.Ico; const money = (n) => (window.formatMoney ? window.formatMoney(n) : n); const fmtDate = (iso) => (window.formatDate ? window.formatDate(iso) : iso); const shortDate = (iso) => { if (!iso) return "—"; const d = new Date(iso); if (isNaN(d)) return iso; return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}.${String(d.getFullYear()).slice(2)}`; }; // Один документ у міні-треку (Рахунок / Акт / Оплата) function DocChip({ label, icon, no, date, done, dim, muted }) { return (
{label} {muted ? "не потрібен" : (no ? `№ ${no} · ${shortDate(date)}` : "—")}
); } function StageRow({ contract, stage, idx, onAction, role }) { const d = window.stageDocs(contract, stage); const st = window.stageStatus(contract, stage); const stInfo = window.STAGE_STATUS[st]; const actions = window.stageActions(contract, stage); const isLate = st === "overdue"; const factDiffers = stage.factDate && stage.dueDate && stage.factDate !== stage.dueDate; return (
{idx + 1}
{stage.stage} {stage.edessb && ЄДЕССБ}
{stage.billing === "advance" ? "Аванс" : "За виконані роботи"} {stage.pct}% план {shortDate(stage.dueDate)} {factDiffers && · факт {shortDate(stage.factDate)}} {isLate && · +{window.daysBetween(stage.dueDate, TODAY)} дн.}
{money(stage.amount)} ₴
{stInfo.label}
{d.needsInvoice ? : } {d.needsAct ? : }
{actions.length === 0 && stage.paidDate && закрито} {actions.map(a => ( ))}
); } // Реєстр додаткових угод function AmendmentsBlock({ contract }) { const ams = contract.amendments || []; const [open, setOpen] = useState(false); if (!ams.length) return null; return (
{open && (
{ams.map((a, i) => (
Дод. угода № {a.no} {fmtDate(a.date)}
{a.reason}
    {a.changes.map((ch, j) =>
  • {ch}
  • )}
))}
)}
); } // Перемикач ролі сторін у договорі function RoleConfig({ contract, onChange }) { const parties = window.contractParties(contract); const set = (patch) => { const cur = window.contractParties(contract); contract.parties = { ...cur, ...patch }; if (patch.ourRole === "contractor" && cur.subject === "services") contract.parties.subject = "works"; if (patch.ourRole === "executor") { contract.parties.subject = "services"; contract.parties.counterRole = "customer"; } onChange(); }; const Seg = ({ value, options, onPick }) => (
{options.map(o => ( ))}
); return (
Наша роль set({ ourRole: v })} options={[{ value: "executor", label: "Виконавець" }, { value: "contractor", label: "Підрядник" }]} />
{parties.ourRole === "contractor" && (
Контрагент set({ counterRole: v })} options={[{ value: "customer", label: "Замовник" }, { value: "genContractor", label: "Генпідрядник" }]} />
)}
Предмет акта set({ subject: v })} options={[{ value: "services", label: "Послуги" }, { value: "works", label: "Роботи" }]} />
); } // Головний компонент — рендериться у розгорнутій картці договору function ContractSchedule({ contract, role, onPrintInvoice, onPrintAct }) { const [, force] = useState(0); const channel = window.billingChannel(contract); const ch = window.BILLING_CHANNEL[channel]; const sum = window.contractBillingSummary(contract); const handle = (idx, key) => { const stage = contract.schedule[idx]; if (key === "invoice") { // позначаємо рахунок виставленим + відкриваємо друковану форму if (!stage.invoiceNo) { const num = `${(contract.number || "").split("/")[0]}/${String(idx + 1).padStart(2, "0")}`; stage.invoiceNo = num; stage.invoiceDate = TODAY; } force(x => x + 1); onPrintInvoice && onPrintInvoice(window.draftInvoiceForContract(contract, idx)); } else if (key === "act") { if (!stage.actDate) { const obj = window.getObject(contract.obj); const codeNum = obj && obj.code ? (obj.code.match(/(\d+)\s*$/) || [])[1] : (contract.number || "").split("/")[0]; stage.actNo = `${codeNum}-АКТ-${String(idx + 1).padStart(2, "0")}`; stage.actDate = TODAY; if (!stage.factDate) stage.factDate = TODAY; stage.ready = true; } force(x => x + 1); onPrintAct && onPrintAct(window.draftActForContract(contract, idx)); } else if (key === "pay") { stage.paidDate = TODAY; if (!stage.factDate) stage.factDate = stage.actDate || TODAY; force(x => x + 1); } }; return (
Графік фінансування {ch.label}
force(x => x + 1)} />
{contract.schedule.map((s, idx) => ( ))}
Оплачено {money(sum.paid)} ₴
{sum.billed > 0 &&
У роботі {money(sum.billed)} ₴
} {sum.overdue > 0 &&
Прострочено {money(sum.overdue)} ₴
} {sum.planned > 0 &&
Заплановано {money(sum.planned)} ₴
}
); } window.ContractSchedule = ContractSchedule; })();