// 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 (