const http = require("http"); const fs = require("fs"); const path = require("path"); const formidable = require("formidable"); const docx = require("docx"); const PORT = process.env.PORT || 3000; const DATA = path.join(__dirname, "data.json"); const UPLOADS = path.join(__dirname, "uploads"); const OUTPUTS = path.join(__dirname, "outputs"); if (!fs.existsSync(UPLOADS)) fs.mkdirSync(UPLOADS); if (!fs.existsSync(OUTPUTS)) fs.mkdirSync(OUTPUTS); function readData() { try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false }, files: [], emails: [] }; } } function writeData(data) { fs.writeFileSync(DATA, JSON.stringify(data, null, 2)); } function body(req) { return new Promise((resolve) => { let s = ""; req.on("data", (c) => (s += c)); req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } }); }); } function generateDocx(data) { const d = data; const prices = d.offers.map(o => Number(o.price)); const min = prices.length ? Math.min(...prices) : 0; const avg = prices.length ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0; const minOffer = d.offers.find(o => Number(o.price) === min); const minSupplier = minOffer ? d.suppliers.find(x => x.id === minOffer.supplierId) : null; const doc = new docx.Document({ sections: [{ properties: {}, children: [ new docx.Paragraph({ text: "МАРКЕТИНГОВОЕ ЗАКЛЮЧЕНИЕ", heading: docx.HeadingLevel.HEADING_1, alignment: docx.AlignmentType.CENTER, spacing: { after: 400 } }), new docx.Paragraph({ text: "Утверждено", alignment: docx.AlignmentType.RIGHT, spacing: { after: 200 } }), new docx.Paragraph({ text: "_______________", spacing: { after: 100 } }), new docx.Paragraph({ text: '«___» __________ 2026г.', spacing: { after: 400 } }), new docx.Paragraph({ text: "1. Входные данные", heading: docx.HeadingLevel.HEADING_2, spacing: { before: 200, after: 200 } }), new docx.Paragraph({ text: "Код ЕНС ТРУ: " + (d.inputData?.ensCode || "—") }), new docx.Paragraph({ text: "Объем: " + (d.inputData?.volume || "—") }), new docx.Paragraph({ text: "Место поставки: " + (d.inputData?.deliveryPlace || "—") }), new docx.Paragraph({ text: "Техническая спецификация: " + (d.inputData?.techSpec || "—") }), new docx.Paragraph({ text: "2. Список поставщиков", heading: docx.HeadingLevel.HEADING_2, spacing: { before: 200, after: 200 } }), ...d.suppliers.map(s => new docx.Paragraph({ text: `${s.name} (${s.status}, ${s.resident}) - ${s.contacts || "—"}` })), new docx.Paragraph({ text: "3. Ценовые предложения", heading: docx.HeadingLevel.HEADING_2, spacing: { before: 200, after: 200 } }), new docx.Table({ width: { size: 100, type: docx.WidthType.PERCENTAGE }, rows: [ new docx.TableRow({ children: [ new docx.TableCell({ children: [new docx.Paragraph("Поставщик")] }), new docx.TableCell({ children: [new docx.Paragraph("№ запроса")] }), new docx.TableCell({ children: [new docx.Paragraph("Цена (тенге)")] }), new docx.TableCell({ children: [new docx.Paragraph("Дата")] }) ] }), ...d.offers.map(o => { const s = d.suppliers.find(x => x.id === o.supplierId); return new docx.TableRow({ children: [ new docx.TableCell({ children: [new docx.Paragraph(s ? s.name : "—")] }), new docx.TableCell({ children: [new docx.Paragraph(o.requestNumber || "—")] }), new docx.TableCell({ children: [new docx.Paragraph(Number(o.price).toLocaleString())] }), new docx.TableCell({ children: [new docx.Paragraph(o.date || "—")] }) ] }); }) ] }), new docx.Paragraph({ text: "4. Проверка по ст. 31", heading: docx.HeadingLevel.HEADING_2, spacing: { before: 200, after: 200 } }), new docx.Paragraph({ text: "☐ Поставщик НЕ в реестре недобросовестных: " + (d.checks.c1 ? "✓" : "☐") }), new docx.Paragraph({ text: "☐ НЕ в перечне терроризма/экстремизма: " + (d.checks.c2 ? "✓" : "☐") }), new docx.Paragraph({ text: "☐ НЕ из офшорной зоны: " + (d.checks.c3 ? "✓" : "☐") }), new docx.Paragraph({ text: "5. Анализ цен", heading: docx.HeadingLevel.HEADING_2, spacing: { before: 200, after: 200 } }), new docx.Paragraph({ text: "Количество предложений: " + d.offers.length }), new docx.Paragraph({ text: "Минимальная цена: " + min.toLocaleString() + " тенге" }), new docx.Paragraph({ text: "Максимальная цена: " + (Math.max(...prices) || 0).toLocaleString() + " тенге" }), new docx.Paragraph({ text: "Средняя цена: " + avg.toLocaleString() + " тенге" }), minSupplier ? new docx.Paragraph({ text: "Лучшее предложение: " + minSupplier.name + " - " + min.toLocaleString() + " тенге", shading: { fill: "d4edda" } }) : null, new docx.Paragraph({ text: "6. Заключение", heading: docx.HeadingLevel.HEADING_2, spacing: { before: 200, after: 200 } }), new docx.Paragraph({ text: "На основании цен потенциальных поставщиков/производителей определены маркетинговые цены без учета НДС (КП представлены)." }), minSupplier ? new docx.Paragraph({ text: "РЕКОМЕНДАЦИЯ: " + minSupplier.name + " (" + minSupplier.status.toLowerCase() + ", " + minSupplier.resident.toLowerCase() + ") по цене " + min.toLocaleString() + " тенге.", spacing: { after: 200 } }) : null, new docx.Paragraph({ text: "Исполнитель: __________________ / _________________", spacing: { before: 400 } }), new docx.Paragraph({ text: "Инициатор потребности: __________________ / _________________", spacing: { before: 200 } }), new docx.Paragraph({ text: "Утверждено: __________________ / _________________", spacing: { before: 400, after: 200 } }), new docx.Paragraph({ text: '«___» __________ 2026г.', spacing: { after: 200 } }) ].filter(Boolean) }] }); return docx.Packer.toBuffer(doc); } http.createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.writeHead(200); return res.end(); } if (req.method === "GET" && req.url === "/api/data") { res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(readData())); } if (req.method === "GET" && req.url === "/api/files") { const data = readData(); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(data.files || [])); } if (req.method === "GET" && req.url === "/api/emails") { const data = readData(); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(data.emails || [])); } if (req.method === "POST" && req.url === "/api/upload") { const form = formidable({ uploadDir: UPLOADS, keepExtensions: true }); form.parse(req, async (err, fields, files) => { if (err) { res.writeHead(500, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: err.message })); } const data = readData(); const uploaded = []; if (files.file && Array.isArray(files.file)) { for (const f of files.file) { const fileName = Date.now() + "_" + path.basename(f.filepath); const newPath = path.join(UPLOADS, fileName); fs.renameSync(f.filepath, newPath); const fileRec = { id: Date.now() + Math.random(), name: f.originalFilename || fileName, path: "/api/files/" + fileName, section: fields.section ? fields.section[0] : "other", size: f.size, date: new Date().toISOString() }; data.files.push(fileRec); uploaded.push(fileRec); } } writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true, files: uploaded })); }); return; } if (req.method === "GET" && req.url.startsWith("/api/files/")) { const fileName = req.url.split("/")[3]; const filePath = path.join(UPLOADS, fileName); if (fs.existsSync(filePath)) { const ext = path.extname(fileName).toLowerCase(); const mime = ext === ".pdf" ? "application/pdf" : ext === ".docx" ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : ext === ".xlsx" ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : "application/octet-stream"; res.writeHead(200, { "Content-Type": mime, "Content-Disposition": "attachment; filename=\"" + fileName + "\"" }); return res.end(fs.readFileSync(filePath)); } res.writeHead(404); res.end("File not found"); return; } if (req.method === "GET" && req.url.startsWith("/api/outputs/")) { const fileName = req.url.split("/")[3]; const filePath = path.join(OUTPUTS, fileName); if (fs.existsSync(filePath)) { res.writeHead(200, { "Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "Content-Disposition": "attachment; filename=\"" + fileName + "\"" }); return res.end(fs.readFileSync(filePath)); } res.writeHead(404); res.end("File not found"); return; } if (req.method === "DELETE" && req.url.startsWith("/api/files/")) { const fileName = req.url.split("/")[3]; const data = readData(); const fileRec = data.files.find(f => f.path.endsWith(fileName)); if (fileRec) { const filePath = path.join(UPLOADS, fileName); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); data.files = data.files.filter(f => f.path !== fileRec.path); writeData(data); } res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } if (req.method === "POST" && req.url === "/api/generate-docx") { try { const data = readData(); const buffer = await generateDocx(data); const fileName = "MAC_" + (data.inputData?.ensCode?.replace(/[^a-zA-Z0-9]/g, "") || "otchet") + "_" + Date.now() + ".docx"; const filePath = path.join(OUTPUTS, fileName); fs.writeFileSync(filePath, buffer); const fileRec = { id: Date.now(), name: fileName.replace(/_\d+\.docx$/, ".docx"), path: "/api/outputs/" + fileName, section: "Выходные документы", size: buffer.length, date: new Date().toISOString() }; data.files.push(fileRec); writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true, file: fileRec })); } catch (e) { console.error("DOCX error:", e); res.writeHead(500, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: e.message })); } } if (req.method === "POST" && req.url === "/api/send-emails") { try { const d = await body(req); const data = readData(); const inputData = data.inputData || {}; const emails = []; for (const supplier of data.suppliers) { const emailData = { id: Date.now() + Math.random(), to: supplier.contacts || "", subject: "Запрос коммерческого предложения: " + (inputData.ensCode || "ТРУ"), body: `Уважаемые партнёры!\n\nПросим предоставить коммерческое предложение на поставку:\n\n` + `Код ЕНС: ${inputData.ensCode || "—"}\n` + `Объем: ${inputData.volume || "—"}\n` + `Место поставки: ${inputData.deliveryPlace || "—"}\n` + `Техническая спецификация: ${inputData.techSpec || "—"}\n\n` + `Просим ответить до: ${d.deadline || "укажите срок"}\n\n` + `С уважением,\nОтдел закупок`, supplierName: supplier.name, date: new Date().toISOString(), status: "Отправлено (симуляция)" }; data.emails.push(emailData); emails.push(emailData); } writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true, count: emails.length, emails })); } catch (e) { res.writeHead(500, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: e.message })); } } if (req.method === "POST" && req.url === "/api/inputData") { const d = await body(req); const data = readData(); data.inputData = d; writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } if (req.method === "POST" && req.url === "/api/suppliers") { const s = await body(req); const data = readData(); data.suppliers.push({ id: Date.now(), ...s }); writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } if (req.method === "POST" && req.url === "/api/offers") { const o = await body(req); const data = readData(); data.offers.push({ id: Date.now(), ...o }); writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } if (req.method === "POST" && req.url === "/api/checks") { const c = await body(req); const data = readData(); data.checks = c; writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } if (req.method === "DELETE" && req.url.startsWith("/api/suppliers/")) { const id = parseInt(req.url.split("/")[3]); const data = readData(); data.suppliers = data.suppliers.filter(s => s.id !== id); data.offers = data.offers.filter(o => o.supplierId !== id); writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } if (req.method === "DELETE" && req.url.startsWith("/api/offers/")) { const id = parseInt(req.url.split("/")[3]); const data = readData(); data.offers = data.offers.filter(o => o.id !== id); writeData(data); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true })); } let file = req.url === "/" ? "/index.html" : req.url.split("?")[0]; const full = path.join(__dirname, file); if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { const ext = path.extname(full); const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); return res.end(fs.readFileSync(full)); } res.writeHead(404); res.end("Not found"); }).listen(PORT, () => console.log("Сервер запущен на порту " + PORT));