analitika-toplivnyh-talonov/server.js

216 lines
6.6 KiB
JavaScript
Raw Permalink 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 url = require("url");
const PORT = process.env.PORT || 3000;
const DATA_FILE = path.join(__dirname, "data.json");
const EMPTY_DATA = {
fuelCards: [],
log: []
};
function load() {
try {
const content = fs.readFileSync(DATA_FILE, "utf8");
return JSON.parse(content);
} catch {
return EMPTY_DATA;
}
}
function save(data) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
}
function logAction(action, details) {
const data = load();
data.log.push({
when: new Date().toISOString(),
what: `${action}: ${details}`,
who: "user"
});
save(data);
}
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname.split("?")[0];
const method = req.method;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
if (pathname === "/api/fuel-cards" && method === "GET") {
const data = load();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data.fuelCards));
return;
}
if (pathname === "/api/fuel-cards" && method === "POST") {
let body = "";
req.on("data", chunk => { body += chunk; });
req.on("end", () => {
try {
const card = JSON.parse(body);
const data = load();
card.id = Date.now();
card.createdAt = new Date().toISOString();
data.fuelCards.push(card);
save(data);
logAction("Добавлен талон", `водитель: ${card.driver}, авто: ${card.vehicle}, литраж: ${card.liters}л`);
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify(card));
} catch (err) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
});
return;
}
if (pathname === "/api/fuel-cards" && method === "PUT") {
let body = "";
req.on("data", chunk => { body += chunk; });
req.on("end", () => {
try {
const updated = JSON.parse(body);
const data = load();
const index = data.fuelCards.findIndex(c => c.id === updated.id);
if (index === -1) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Не найдено" }));
return;
}
const old = data.fuelCards[index];
data.fuelCards[index] = { ...old, ...updated };
save(data);
logAction("Изменён талон", `ID: ${updated.id}, водитель: ${updated.driver}`);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data.fuelCards[index]));
} catch (err) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
});
return;
}
if (pathname.startsWith("/api/fuel-cards/") && method === "DELETE") {
const id = parseInt(parsedUrl.pathname.split("/").pop());
const data = load();
const card = data.fuelCards.find(c => c.id === id);
if (!card) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Не найдено" }));
return;
}
data.fuelCards = data.fuelCards.filter(c => c.id !== id);
save(data);
logAction("Удалён талон", `водитель: ${card.driver}, авто: ${card.vehicle}, литраж: ${card.liters}л`);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true }));
return;
}
if (pathname === "/api/log" && method === "GET") {
const data = load();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data.log.slice(-50).reverse()));
return;
}
if (pathname === "/api/stats" && method === "GET") {
const data = load();
const cards = data.fuelCards;
const totalLiters = cards.reduce((sum, c) => sum + (parseFloat(c.liters) || 0), 0);
const totalAmount = cards.reduce((sum, c) => sum + (parseFloat(c.amount) || 0), 0);
const totalCards = cards.length;
const byDriver = {};
cards.forEach(c => {
if (!byDriver[c.driver]) {
byDriver[c.driver] = { liters: 0, amount: 0, count: 0 };
}
byDriver[c.driver].liters += parseFloat(c.liters) || 0;
byDriver[c.driver].amount += parseFloat(c.amount) || 0;
byDriver[c.driver].count += 1;
});
const byVehicle = {};
cards.forEach(c => {
if (!byVehicle[c.vehicle]) {
byVehicle[c.vehicle] = { liters: 0, amount: 0, count: 0 };
}
byVehicle[c.vehicle].liters += parseFloat(c.liters) || 0;
byVehicle[c.vehicle].amount += parseFloat(c.amount) || 0;
byVehicle[c.vehicle].count += 1;
});
const byGasStation = {};
cards.forEach(c => {
if (!byGasStation[c.gasStation]) {
byGasStation[c.gasStation] = { liters: 0, amount: 0, count: 0 };
}
byGasStation[c.gasStation].liters += parseFloat(c.liters) || 0;
byGasStation[c.gasStation].amount += parseFloat(c.amount) || 0;
byGasStation[c.gasStation].count += 1;
});
const stats = {
totalLiters,
totalAmount,
totalCards,
byDriver,
byVehicle,
byGasStation,
avgPricePerLiter: totalLiters > 0 ? (totalAmount / totalLiters).toFixed(2) : 0
};
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(stats));
return;
}
const filePath = path.join(__dirname, parsedUrl.pathname === "/" ? "index.html" : parsedUrl.pathname);
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
".html": "text/html",
".css": "text/css",
".js": "application/javascript",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml"
};
fs.readFile(filePath, (err, content) => {
if (err) {
if (err.code === "ENOENT") {
res.writeHead(404);
res.end("Not found");
} else {
res.writeHead(500);
res.end("Server error");
}
return;
}
res.writeHead(200, { "Content-Type": mimeTypes[ext] || "text/plain" });
res.end(content);
});
});
server.listen(PORT, () => {
console.log(`Сервер запущен на порту ${PORT}`);
});