// procurement-prf.jsx — FORM 10 «Запит на закупівлю» (Purchase Request Form). // Екран-форма, заповнюється з рядка Плану закупівель, редагується, експортується // в .xlsx (SheetJS). Дані зберігаються у procurement.prf (персист через onSave). (function () { const TYPE_UNIT = { Services: "people/days", Works: "service", Goods: "pcs" }; // легкий парсер «(days, 70)» / «(15 pcs)» / «(1 service)» → {unit, qty} function parseUnitQty(title, type) { const m = String(title || "").match(/\(([^)]*)\)/); let unit = TYPE_UNIT[type] || "", qty = ""; if (m) { const inside = m[1]; const num = inside.match(/\d+(?:[.,]\d+)?/); if (num) qty = Number(num[0].replace(",", ".")); const word = inside.replace(/[\d.,]/g, "").replace(/[,;]/g, " ").trim(); if (word) unit = word; } return { unit, qty }; } function blankItem(n) { return { id: "it-" + Math.random().toString(36).slice(2, 6), no: n, planLine: "", desc: "", unit: "", qty: "", projectCode: "", budgetLine: "" }; } // Побудувати початковий PRF із рядка плану + проєкту function seedPrf(row, project, planIndex) { const uq = parseUnitQty(row.title, row.type); return { deliveryDate: row.needDate || "", address: "", office: "ГО «Екоклуб»", committee: "", responsible: "", criteriaRef: "", criteria: [], items: [{ ...blankItem(1), planLine: String(planIndex != null ? planIndex + 1 : ""), desc: row.title || "", unit: uq.unit, qty: uq.qty, projectCode: project.code || "", budgetLine: row.budgetLine || "", }], sign: { requestor: { name: "", position: "" }, authorizer: { name: "", position: "" }, procurement: { name: "", position: "" } }, }; } // ── експорт у .xlsx (спільний рендерер ExcelJS) ── function exportXlsx(prf, row, project) { if (!window.ProcXlsx) { alert("Модуль Excel не завантажився. Онови сторінку."); return; } const x = window.ProcXlsx; const critText = (prf.criteria || []).map(c => `${c.name} — ${c.weight}%`).join("; "); const itemCols = [ { header: "S.No", width: 6 }, { header: "Line № in Plan / № рядка", width: 16 }, { header: "Description / Опис", width: 40 }, { header: "Unit / Од.", width: 12 }, { header: "Qty / К-сть", width: 10 }, { header: "Project code / Код проєкту", width: 18 }, { header: "Budget line / Бюдж. лінія", width: 18 }, ]; const itemRows = (prf.items || []).map((it, i) => [i + 1, it.planLine, it.desc, it.unit, it.qty, it.projectCode, it.budgetLine]); const signCols = [{ header: "", width: 26 }, { header: "Full name / ПІБ", width: 26 }, { header: "Position / Посада", width: 24 }, { header: "Signature / Підпис", width: 20 }]; const signRows = [ ["Requestor / Замовник", prf.sign.requestor.name, prf.sign.requestor.position, ""], ["Authorizer / Затверджувач", prf.sign.authorizer.name, prf.sign.authorizer.position, ""], ["Procurement / Закупівлі", prf.sign.procurement.name, prf.sign.procurement.position, ""], ]; const blocks = [ x.title("PURCHASE REQUEST FORM / ЗАПИТ НА ЗАКУПІВЛЮ (Форма 10)"), x.gap(), x.field("Requested delivery date / Дата потреби", prf.deliveryDate || ""), x.field("Requested delivery address / Адреса постачання", prf.address || ""), x.field("Office / Офіс", prf.office || ""), x.field("Evaluation committee members / Члени комітету оцінки", prf.committee || ""), x.field("Suggested evaluation criteria (weight) / Критерії оцінки", critText), x.field("Responsible / Budget holder / Відповідальний", prf.responsible || ""), x.gap(), x.table(itemCols, itemRows), x.gap(), x.section("SIGNATURES / ПІДПИСИ"), x.table(signCols, signRows), ]; const safe = (project.code || "DOVIRA").replace(/[^\wА-Яа-яІЇЄґ.-]+/g, "_"); x.save(`FORM10_PRF_${safe}_${(prf.items[0] && prf.items[0].planLine) || ""}.xlsx`, "PRF", blocks); } function PrfEditor({ row, planIndex, project, role, onSave, onClose }) { const canEdit = role === "director"; // тільки директор (рішення: доступ лише директорці) const [prf, setPrf] = React.useState(() => row.prf ? JSON.parse(JSON.stringify(row.prf)) : seedPrf(row, project, planIndex)); const up = (patch) => setPrf(p => ({ ...p, ...patch })); const upItem = (id, patch) => setPrf(p => ({ ...p, items: p.items.map(it => it.id === id ? { ...it, ...patch } : it) })); const addItem = () => setPrf(p => ({ ...p, items: [...p.items, blankItem(p.items.length + 1)] })); const rmItem = (id) => setPrf(p => ({ ...p, items: p.items.filter(it => it.id !== id) })); const upSign = (who, patch) => setPrf(p => ({ ...p, sign: { ...p.sign, [who]: { ...p.sign[who], ...patch } } })); // бібліотека критеріїв (фільтр за типом закупівлі) const allCrit = window.DATA.PROCUREMENT_CRITERIA || []; const critOpts = allCrit.filter(t => !t.kind || t.kind === row.type); const applyCriteria = (id) => { const t = allCrit.find(x => x.id === id); up({ criteriaRef: id, criteria: t ? (t.criteria || []).map(c => ({ name: c.name, weight: c.weight })) : [] }); }; React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const save = () => { onSave(prf); }; return (
| № | № плану | Опис | Од. | К-сть | Код проєкту | Бюдж. лінія | {canEdit &&} |
|---|---|---|---|---|---|---|---|
| {i + 1} | upItem(it.id, { planLine: e.target.value })} disabled={!canEdit} /> | upItem(it.id, { desc: e.target.value })} disabled={!canEdit} /> | upItem(it.id, { unit: e.target.value })} disabled={!canEdit} /> | upItem(it.id, { qty: e.target.value === "" ? "" : Number(e.target.value) })} disabled={!canEdit} /> | upItem(it.id, { projectCode: e.target.value })} disabled={!canEdit} /> | upItem(it.id, { budgetLine: e.target.value })} disabled={!canEdit} /> | {canEdit &&} |