napishi-programmu-ispolnenie/server.js

181 lines
6.4 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 XLSX = require("xlsx");
const PORT = process.env.PORT || 3000;
const DATA_FILE = path.join(__dirname, "data.json");
const EXCEL_FILE = path.join(__dirname, "Выгрузка исполнения бюджета закупок.xlsx");
function parseExcelData() {
try {
if (!fs.existsSync(EXCEL_FILE)) {
console.log("Excel файл не найден, используем существующие данные");
return null;
}
const workbook = XLSX.readFile(EXCEL_FILE);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (rows.length < 2) return [];
const headers = rows[0].map(h => String(h || '').trim().toLowerCase());
const findColumn = (names) => {
for (const name of names) {
const idx = headers.findIndex(h => h.toLowerCase().includes(name.toLowerCase()));
if (idx !== -1) return idx;
}
return -1;
};
const serviceNameIdx = findColumn(['Краткий текст материала']);
const budgetSumIdx = findColumn(['Сумма с уч.корр']);
const executedSumIdx = findColumn(['Сумма поставки,тг']);
const sppElementIdx = findColumn(['СПП-элемент']);
const uniqueObjectIdIdx = findColumn(['Уникальный номер объекта']);
const lotNumberIdx = findColumn(['Номер лота']);
const departmentIdx = findColumn(['Завод', 'МВЗ', 'Текст МВЗ']);
const procurementMethodIdx = findColumn(['Способ закупки']);
const procurementTypeIdx = findColumn(['Вид закупа']);
const data = [];
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
if (!row || row.length === 0) continue;
const budgetSum = parseFloat(String(row[budgetSumIdx] || 0).replace(/\s/g, '')) || 0;
const executedSum = parseFloat(String(row[executedSumIdx] || 0).replace(/\s/g, '')) || 0;
const sppElement = String(row[sppElementIdx] || '').trim();
const uniqueObjectId = String(row[uniqueObjectIdIdx] || '').trim();
const lotNumber = String(row[lotNumberIdx] || '').trim();
const procurementMethod = String(row[procurementMethodIdx] || '').trim();
const procurementType = String(row[procurementTypeIdx] || '').trim();
// Экономия = Бюджет - Сумма поставки
const economy = budgetSum - executedSum;
let budgetType = '';
if (sppElement.startsWith('I')) {
budgetType = 'БКВ';
} else if (sppElement.startsWith('O')) {
budgetType = 'ОБ';
}
let execPercent = 0;
// Всегда считаем процент из сумм, игнорируя колонку % из Excel
if (budgetSum > 0) {
execPercent = (executedSum / budgetSum) * 100;
}
const item = {
uniqueObjectId: uniqueObjectId,
lotNumber: lotNumber,
sppElement: sppElement,
serviceName: String(row[serviceNameIdx] || '').trim(),
department: String(row[departmentIdx] || '').trim(),
budgetType: budgetType,
procurementMethod: procurementMethod,
procurementType: procurementType,
budgetSum: budgetSum,
executedSum: executedSum,
economy: economy,
execPercent: Math.round(execPercent * 100) / 100
};
if (item.serviceName) {
data.push(item);
}
}
console.log("Обработано строк:", data.length);
return data;
} catch (error) {
console.error("Ошибка при чтении Excel:", error.message);
return null;
}
}
function ensureDataFile() {
if (!fs.existsSync(DATA_FILE)) {
const data = parseExcelData();
if (data && data.length > 0) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
console.log("Создан data.json с", data.length, "записями");
} else {
fs.writeFileSync(DATA_FILE, JSON.stringify([], 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({});
}
});
});
}
http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") {
res.writeHead(204);
return res.end();
}
if (req.method === "GET" && req.url === "/api/data") {
try {
const data = JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(data));
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: error.message }));
}
}
if (req.method === "POST" && req.url === "/api/reload") {
try {
const data = parseExcelData();
if (data !== null) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, count: data.length }));
} else {
res.writeHead(400, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: "Не удалось прочитать Excel файл" }));
}
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: error.message }));
}
}
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, () => {
ensureDataFile();
console.log("Сервер запущен на порту " + PORT);
});