napishi-programmu-ispolnenie/server.js

158 lines
5.5 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.includes(name.toLowerCase()));
if (idx !== -1) return idx;
}
return -1;
};
const articleCodeIdx = findColumn(['код статьи', 'статья', 'код']);
const serviceNameIdx = findColumn(['наименование', 'услуг', 'работ', 'название']);
const departmentIdx = findColumn(['подраздел', 'центр', 'департамент', 'управление']);
const budgetSumIdx = findColumn(['бюджет', 'план', 'сумма план']);
const executedSumIdx = findColumn(['исполн', 'факт', 'оплачено']);
const execPercentIdx = 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;
let execPercent = 0;
if (execPercentIdx !== -1 && row[execPercentIdx]) {
execPercent = parseFloat(String(row[execPercentIdx]).replace('%', '').replace(/\s/g, '')) || 0;
} else if (budgetSum > 0) {
execPercent = (executedSum / budgetSum) * 100;
}
const item = {
articleCode: String(row[articleCodeIdx] || '').trim(),
serviceName: String(row[serviceNameIdx] || '').trim(),
department: String(row[departmentIdx] || '').trim(),
budgetSum: budgetSum,
executedSum: executedSum,
execPercent: Math.round(execPercent * 100) / 100
};
if (item.serviceName || item.articleCode) {
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);
});