nado-sozdat-po-marketingovom/server.js

381 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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));