// procurement-forms.jsx — «Пакет документів» закупівлі (хаб форм PIN). // Спільні дані (шапка, позиції, учасники, комітет) живлять усі форми; кожна // форма експортується в .xlsx (SheetJS). Дані зберігаються у procurement.case // (+ procurement.prf для FORM 10). Підхід: відтворення форм + експорт Excel. (function () { const rnd = () => Math.random().toString(36).slice(2, 6); // канонічна процедура з тексту function procLabel(s) { s = (s || "").toLowerCase(); if (s.includes("tender") || s.includes("тендер")) return "Відкритий тендер / Open tender"; if (s.includes("negoti") || s.includes("перегов")) return "Переговорна / Negotiated"; if (s.includes("simplif") || s.includes("спрощ")) return "Спрощена / Simplified"; if (s.includes("simple") || s.includes("прост")) return "Проста / Simple purchase"; return s || ""; } function procKey(s) { s = (s || "").toLowerCase(); if (s.includes("tender") || s.includes("тендер")) return "tender"; if (s.includes("negoti") || s.includes("перегов")) return "negotiated"; if (s.includes("simplif") || s.includes("спрощ")) return "simplified"; if (s.includes("simple") || s.includes("прост")) return "simple"; return ""; } const blankCase = () => ({ eloFolder: "", location: "", bidders: [], committee: [] }); // позиції закупівлі: беремо з PRF, інакше — одна позиція з рядка плану function itemsOf(row, project, planIndex) { if (row.prf && row.prf.items && row.prf.items.length) return row.prf.items; return [{ planLine: String(planIndex != null ? planIndex + 1 : ""), desc: row.title || "", unit: "", qty: "", projectCode: project.code || "", budgetLine: row.budgetLine || "" }]; } // ── експорт через спільний рендерер ExcelJS (блоки) ── const X = () => window.ProcXlsx; const fileName = (project, code) => `${code}_${(project.code || "DOVIRA").replace(/[^\wА-Яа-яІЇЄґ.-]+/g, "_")}.xlsx`; const col = (header, width) => ({ header, width }); function hdrFields(row, project, cs, n) { const X0 = X(); const f = [ X0.field("Contract title / Назва закупівлі", row.title || ""), X0.field("Project code / Код проєкту", project.code || ""), X0.field("Procurement ELO folder no. / № папки ELO", cs.eloFolder || ""), X0.field("Procedure / Процедура", procLabel(row.procedure)), X0.field("Location / Місцезнаходження", cs.location || ""), ]; return n ? f.slice(0, n) : f; } // ════════ Білдери форм (блоки) ════════ const BUILD = { f6(ctx) { const x = X(), { project, cs, items } = ctx; const cols = [col("S.No", 6), col("Description / Опис", 42), col("Unit / Од.", 12), col("Qty / К-сть", 10), col("Currency", 12), col("Unit price / Ціна", 14), col("Total / Сума", 14)]; const rows = items.map((it, i) => [i + 1, it.desc, it.unit, it.qty, "USD", "", ""]); const blocks = [ x.title("QUOTATION FORM / ФОРМА ЦІНОВОЇ ПРОПОЗИЦІЇ (Форма 6)"), x.gap(), x.field("PRF Number / № запиту", items[0] ? items[0].planLine : ""), x.gap(), x.section("SUPPLIER DETAILS / РЕКВІЗИТИ ПОСТАЧАЛЬНИКА"), x.field("Name / Назва", ""), x.field("Address / Адреса", ""), x.field("Owner / Власник", ""), x.field("Phone / Телефон", ""), x.field("E-mail", ""), x.gap(), x.table(cols, rows), x.gap(), x.section("SIGNATURE / ПІДПИС"), x.field("Place / Місце", ""), x.field("Date / Дата", ""), x.field("Full name & position / ПІБ і посада", ""), ]; return { filename: fileName(project, "FORM6_Quotation"), sheet: "Quotation", blocks }; }, f7(ctx) { const x = X(), { row, project, cs, prf } = ctx; const crit = (prf && prf.criteria && prf.criteria.length) ? prf.criteria : [{ name: "Ціна / Price", weight: "" }, { name: "Якість / Quality", weight: "" }]; const bidders = cs.bidders.length ? cs.bidders : [{ name: "<Учасник 1>" }, { name: "<Учасник 2>" }, { name: "<Учасник 3>" }]; const cols = [col("Criteria / Критерій", 42), col("Weight / Вага", 12), ...bidders.map(b => col(b.name || "—", 18))]; const rows = crit.map(c => [c.name, (c.weight ? c.weight + "%" : ""), ...bidders.map(() => "")]); rows.push(["TOTAL / РАЗОМ", "100%", ...bidders.map(() => "")]); rows.push(["Rank / Ранг", "", ...bidders.map(() => "")]); const blocks = [x.title("QUOTATION EVALUATION PROTOCOL / ПРОТОКОЛ ОЦІНКИ (Форма 7/13)"), x.gap(), ...hdrFields(row, project, cs), x.gap(), x.table(cols, rows)]; return { filename: fileName(project, "FORM7_Evaluation"), sheet: "Evaluation", blocks }; }, f3(ctx) { const x = X(), { row, project, cs } = ctx; const bidders = cs.bidders.length ? cs.bidders : [{}, {}, {}]; const cols = [col("No", 6), col("Bidder / Учасник", 36), col("Submission date / Дата", 16), col("Time / Час", 12), col("PIN signature", 20), col("Bidder signature", 20)]; const rows = bidders.map((b, i) => [i + 1, b.name || "", "", "", "", ""]); const blocks = [x.title("TENDER DOCUMENTATION DELIVERY REPORT / ЗВІТ ПРО ПОДАННЯ (Форма 3)"), x.gap(), ...hdrFields(row, project, cs, 3), x.gap(), x.table(cols, rows)]; return { filename: fileName(project, "FORM3_Delivery"), sheet: "Delivery", blocks }; }, f4(ctx) { const x = X(), { row, project, cs } = ctx; const bidders = cs.bidders.length ? cs.bidders : [{}, {}, {}]; const cols = [col("No", 6), col("Bidder / Учасник", 36), col("Qualification (Pass/Fail) / Кваліфікація", 40), col("Meets all? / Відповідає всім?", 22)]; const rows = bidders.map((b, i) => [i + 1, b.name || "", "", ""]); const blocks = [x.title("PROTOCOL OF OPENING ENVELOPES / ВІДКРИТТЯ КОНВЕРТІВ (Форма 4)"), x.gap(), ...hdrFields(row, project, cs), x.gap(), x.table(cols, rows)]; return { filename: fileName(project, "FORM4_Opening"), sheet: "Opening", blocks }; }, f8(ctx) { const x = X(), { row, project, cs } = ctx; const bidders = cs.bidders.length ? cs.bidders : [{}, {}, {}]; const cols = [col("No", 6), col("Bidder / Учасник", 32), col("E-mail / контакт", 24), col("Request date / Дата запиту", 16), col("Sending date / Дата надсилання", 16), col("PIN sign", 14), col("Bidder sign", 14)]; const rows = bidders.map((b, i) => [i + 1, b.name || "", b.email || "", "", "", "", ""]); const blocks = [x.title("TENDER NOTICE / DOCUMENTATION DISTRIBUTION / РОЗПОВСЮДЖЕННЯ (Форма 8)"), x.gap(), ...hdrFields(row, project, cs, 3), x.gap(), x.table(cols, rows)]; return { filename: fileName(project, "FORM8_Distribution"), sheet: "Distribution", blocks }; }, f12(ctx) { const x = X(), { project, cs, items } = ctx; const cols = [col("S.No", 6), col("Description / Опис", 40), col("Unit / Од.", 10), col("Qty / К-сть", 9), col("Budget no. / Бюджет", 16), col("Budget line / Лінія", 16), col("Currency", 10), col("Unit price / Ціна", 14)]; const rows = items.map((it, i) => [i + 1, it.desc, it.unit, it.qty, it.projectCode, it.budgetLine, "USD", ""]); const blocks = [ x.title("PURCHASE ORDER / ЗАМОВЛЕННЯ НА ЗАКУПІВЛЮ (Форма 12)"), x.gap(), x.field("Contract ELO number / № контракту ELO", cs.eloFolder || ""), x.field("Delivery address / Адреса постачання", cs.location || ""), x.gap(), x.section("SUPPLIER DETAILS / РЕКВІЗИТИ ПОСТАЧАЛЬНИКА"), x.field("Name / Назва", ""), x.field("Address / Адреса", ""), x.field("Phone / Телефон", ""), x.field("E-mail", ""), x.gap(), x.table(cols, rows), x.gap(), x.field("Total / Разом", ""), ]; return { filename: fileName(project, "FORM12_PurchaseOrder"), sheet: "PO", blocks }; }, f17(ctx) { const x = X(); const cols = [col("Score / Бал", 12), col("Benchmark / Опис (послуги/роботи)", 70)]; const rows = [ [5, "Відмінна відповідь без слабких місць, повне розуміння вимог"], [4, "Дуже добра відповідь, реальне розуміння вимог"], [3, "Задовільна відповідь, базове розуміння"], [2, "Відповідь із застереженнями — бракує повноти"], [1, "Серйозні застереження — суттєві прогалини"], [0, "Відповідь повністю не відповідає критерію"], ]; const blocks = [x.title("QUALITY EVALUATION — SCORING GRID / СІТКА БАЛІВ (Форма 17)"), x.gap(), x.table(cols, rows)]; return { filename: fileName(ctx.project, "FORM17_ScoringGrid"), sheet: "Grid", blocks }; }, }; // реєстр форм: code, назва, тип, для яких процедур, білдер const FORMS = [ { code: "10", name: "Запит на закупівлю (PRF)", kind: "editor", procs: ["simplified", "negotiated", "tender"] }, { code: "6", name: "Форма цінової пропозиції", kind: "xlsx", b: "f6", procs: ["simplified", "negotiated"] }, { code: "7/13", name: "Протокол оцінки пропозицій", kind: "xlsx", b: "f7", procs: ["simplified", "negotiated", "tender"] }, { code: "16", name: "Таблиця оцінки з формулами", kind: "xlsx", b: "f7", procs: ["negotiated", "tender"] }, { code: "17", name: "Сітка балів (критерії якості)", kind: "xlsx", b: "f17", procs: ["simplified", "negotiated", "tender"] }, { code: "3", name: "Звіт про подання пропозицій", kind: "xlsx", b: "f3", procs: ["negotiated", "tender"] }, { code: "4", name: "Протокол відкриття конвертів", kind: "xlsx", b: "f4", procs: ["negotiated", "tender"] }, { code: "8", name: "Розповсюдження тендерної док.", kind: "xlsx", b: "f8", procs: ["tender"] }, { code: "12", name: "Замовлення на закупівлю (PO)", kind: "xlsx", b: "f12", procs: ["simplified", "negotiated", "tender"] }, { code: "15", name: "Призначення комітету оцінки", kind: "word", procs: ["negotiated", "tender"], note: "Word · якщо > €10 000" }, { code: "1/11", name: "Оголошення тендеру", kind: "word", procs: ["negotiated", "tender"] }, { code: "2", name: "Декларація відповідності", kind: "word", procs: ["negotiated", "tender"] }, { code: "14", name: "Технічне завдання (ToR)", kind: "word", procs: ["negotiated", "tender"] }, { code: "5", name: "Звіт про оцінку тендеру", kind: "word", procs: ["negotiated", "tender"] }, { code: "9", name: "Повідомлення про результат", kind: "word", procs: ["negotiated", "tender"] }, { code: "18", name: "Сітка зважених критеріїв", kind: "xlsx", b: "f17", procs: ["tender"] }, { code: "19", name: "Специфікація для IT-закупівель", kind: "word", procs: ["tender"], note: "лише IT" }, ]; function FormsHub({ row, planIndex, project, role, onSave, onClose }) { const canEdit = role === "director"; // тільки директор (рішення: доступ лише директорці) const [cs, setCs] = React.useState(() => Object.assign(blankCase(), row.case || {})); const [openPrf, setOpenPrf] = React.useState(false); const pk = procKey(row.procedure); React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape") { if (openPrf) setOpenPrf(false); else onClose(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [openPrf, onClose]); const saveCase = (next) => { setCs(next); onSave({ case: next }); }; const addBidder = () => saveCase({ ...cs, bidders: [...cs.bidders, { id: "bd-" + rnd(), name: "", email: "" }] }); const upBidder = (id, patch) => saveCase({ ...cs, bidders: cs.bidders.map(b => b.id === id ? { ...b, ...patch } : b) }); const rmBidder = (id) => saveCase({ ...cs, bidders: cs.bidders.filter(b => b.id !== id) }); const addMember = () => saveCase({ ...cs, committee: [...cs.committee, { id: "cm-" + rnd(), name: "", position: "" }] }); const upMember = (id, patch) => saveCase({ ...cs, committee: cs.committee.map(m => m.id === id ? { ...m, ...patch } : m) }); const rmMember = (id) => saveCase({ ...cs, committee: cs.committee.filter(m => m.id !== id) }); const ctx = () => ({ row, project, planIndex, cs, prf: row.prf, items: itemsOf(row, project, planIndex) }); const exportForm = (f) => { const r = BUILD[f.b](ctx()); window.ProcXlsx.save(r.filename, r.sheet, r.blocks); }; if (openPrf && window.ProcurementPRF) { return { onSave({ prf }); setOpenPrf(false); }} onClose={() => setOpenPrf(false)} />; } const grouped = [ { key: "simplified", label: "Спрощена процедура" }, { key: "negotiated", label: "Переговорна процедура" }, { key: "tender", label: "Відкритий тендер" }, ]; return (
Пакет документів закупівлі
{row.title} · {procLabel(row.procedure) || "процедура не задана"}
{/* спільні дані */}
{/* учасники + комітет */}
Учасники (бідери)
{cs.bidders.map(b => (
upBidder(b.id, { name: e.target.value })} disabled={!canEdit} /> upBidder(b.id, { email: e.target.value })} disabled={!canEdit} /> {canEdit && }
))} {canEdit && }
Комітет з оцінки
{cs.committee.map(m => (
upMember(m.id, { name: e.target.value })} disabled={!canEdit} /> upMember(m.id, { position: e.target.value })} disabled={!canEdit} /> {canEdit && }
))} {canEdit && }
{/* список форм за процедурою */} {grouped.map(g => { const list = FORMS.filter(f => f.procs.includes(g.key)); const isActive = pk === g.key; return (
{g.label}{isActive && ваша процедура}
{list.map(f => (
FORM {f.code} {f.name}{f.note && · {f.note}} {f.kind === "editor" && } {f.kind === "xlsx" && } {f.kind === "word" && Word · готуємо}
))}
); })}
); } window.ProcurementFormsHub = FormsHub; })();