// procurement-xlsx.jsx — спільний рендерер форм закупівель у стилізований .xlsx // (ExcelJS). Форма описується «блоками» (title/field/section/table/gap), а рендерер // застосовує єдине оформлення: жирні заголовки, рамки, заливка секцій, перенос. // Використовується і хабом форм (procurement-forms), і PRF-редактором (procurement-prf). (function () { const GREEN = "FF068B49", SECT = "FFEAF3EE", HEAD = "FFEFF3F1", LINE = "FFBFC9C2"; const thin = { style: "thin", color: { argb: LINE } }; const allBorder = { top: thin, left: thin, bottom: thin, right: thin }; function colLetter(n) { let s = ""; while (n > 0) { const m = (n - 1) % 26; s = String.fromCharCode(65 + m) + s; n = Math.floor((n - 1) / 26); } return s; } function save(filename, sheetName, blocks) { if (!window.ExcelJS) { alert("Бібліотека Excel (ExcelJS) не завантажилась. Онови сторінку (Ctrl+Shift+R)."); return; } const wb = new window.ExcelJS.Workbook(); const ws = wb.addWorksheet(sheetName || "Form", { views: [{ showGridLines: false }] }); // кількість колонок = найширша таблиця (мін. 2) let totalCols = 2; blocks.forEach(b => { if (b.t === "table") totalCols = Math.max(totalCols, b.cols.length); }); // ширина колонок: з таблиць; для col A враховуємо поля const widths = new Array(totalCols).fill(0); blocks.forEach(b => { if (b.t === "table") b.cols.forEach((c, i) => { widths[i] = Math.max(widths[i], c.width || 14); }); }); blocks.forEach(b => { if (b.t === "field") widths[0] = Math.max(widths[0], 34); }); for (let i = 0; i < totalCols; i++) ws.getColumn(i + 1).width = widths[i] || 16; const LAST = colLetter(totalCols); let r = 1; blocks.forEach(b => { if (b.t === "gap") { r++; return; } if (b.t === "title") { ws.mergeCells(`A${r}:${LAST}${r}`); const c = ws.getCell(`A${r}`); c.value = b.text; c.font = { bold: true, size: 13, color: { argb: "FFFFFFFF" } }; c.alignment = { horizontal: "center", vertical: "middle", wrapText: true }; c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: GREEN } }; ws.getRow(r).height = 28; r++; return; } if (b.t === "section") { ws.mergeCells(`A${r}:${LAST}${r}`); const c = ws.getCell(`A${r}`); c.value = b.text; c.font = { bold: true, size: 11 }; c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: SECT } }; c.alignment = { vertical: "middle" }; ws.getRow(r).height = 20; r++; return; } if (b.t === "field") { const lc = ws.getCell(`A${r}`); lc.value = b.label; lc.font = { bold: true }; lc.alignment = { vertical: "middle", wrapText: true }; if (totalCols >= 3) ws.mergeCells(`B${r}:${LAST}${r}`); const vc = ws.getCell(`B${r}`); vc.value = b.value == null ? "" : b.value; vc.alignment = { vertical: "middle", wrapText: true }; r++; return; } if (b.t === "table") { b.cols.forEach((c, i) => { const cell = ws.getCell(`${colLetter(i + 1)}${r}`); cell.value = c.header; cell.font = { bold: true }; cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: HEAD } }; cell.border = allBorder; cell.alignment = { vertical: "middle", wrapText: true }; }); r++; b.rows.forEach(row => { for (let i = 0; i < b.cols.length; i++) { const cell = ws.getCell(`${colLetter(i + 1)}${r}`); const v = row[i]; cell.value = v == null ? "" : v; cell.border = allBorder; cell.alignment = { vertical: "top", wrapText: true }; } r++; }); return; } }); wb.xlsx.writeBuffer().then(buf => { const blob = new Blob([buf], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1500); }).catch(e => { console.error("[ERP] xlsx:", e); alert("Помилка генерації Excel."); }); } window.ProcXlsx = { save, title: (text) => ({ t: "title", text }), field: (label, value) => ({ t: "field", label, value: value == null ? "" : value }), section: (text) => ({ t: "section", text }), table: (cols, rows) => ({ t: "table", cols, rows }), gap: () => ({ t: "gap" }), }; })();