mne-nuzhno-sdelat-sayt-ili-p/server.js

180 lines
5.9 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 PORT = process.env.PORT || 3000;
const DATA = path.join(__dirname, "data.json");
function readData() {
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { calculations: [] }; }
}
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({}); } });
});
}
// Формула Эрланга C (вероятность ожидания)
function erlangC(A, N) {
if (N <= 0 || A <= 0) return 1;
let sum = 0;
for (let i = 0; i < N; i++) {
sum += Math.pow(A, i) / factorial(i);
}
const numerator = (Math.pow(A, N) / factorial(N)) * (N / (N - A));
const denominator = sum + numerator;
return numerator / denominator;
}
function factorial(n) {
if (n <= 1) return 1;
let result = 1;
for (let i = 2; i <= n; i++) result *= i;
return result;
}
// Расчёт требуемого количества агентов (Erlang C)
function calculateAgents(contacts, ahtMin, serviceTimeSec, patienceSec, targetSL) {
const ahtSec = ahtMin * 60;
const trafficIntensity = (contacts * ahtSec) / 3600; // в Эрлангах за час
let agents = Math.ceil(trafficIntensity);
let sl = 0;
for (let n = agents; n <= agents + 50; n++) {
const pWait = erlangC(trafficIntensity, n);
const avgSpeed = (pWait * serviceTimeSec) / (n - trafficIntensity);
const probAnswered = 1 - pWait * Math.exp(-(n - trafficIntensity) * patienceSec / serviceTimeSec);
sl = probAnswered * 100;
if (sl >= targetSL) {
agents = n;
break;
}
}
return { agents, trafficIntensity, sl: Math.min(sl, 99.9) };
}
// Расчёт FTE для разных каналов
function calculateFTE(params) {
const results = {};
// Входящие звонки (Erlang C)
if (params.incomingCalls && params.incomingCalls > 0) {
const r = calculateAgents(
params.incomingCalls,
params.ahtIncoming || 4,
params.serviceTimeIncoming || 40,
params.patienceIncoming || 40,
params.targetSL || 80
);
results.incoming = {
fte: r.agents,
trafficIntensity: r.trafficIntensity.toFixed(2),
sl: r.sl.toFixed(1)
};
}
// Исходящие звонки (простой расчёт по времени)
if (params.outgoingCalls && params.outgoingCalls > 0) {
const totalMinutes = params.outgoingCalls * (params.ahtOutgoing || 4.5);
const workingMinutes = (params.fteHours || 189) * 60 * (params.utilization || 0.86);
results.outgoing = {
fte: Math.ceil(totalMinutes / workingMinutes * 10) / 10,
totalHours: (totalMinutes / 60).toFixed(1)
};
}
// Неголосовые тикеты
if (params.tickets && params.tickets > 0) {
const totalMinutes = params.tickets * (params.ahtTickets || 12);
const workingMinutes = (params.fteHours || 189) * 60 * (params.utilization || 0.86);
results.tickets = {
fte: Math.ceil(totalMinutes / workingMinutes * 10) / 10,
totalHours: (totalMinutes / 60).toFixed(1)
};
}
// Чаты (с учётом concurrency)
if (params.chats && params.chats > 0) {
const concurrency = params.concurrency || 4;
const totalMinutes = (params.chats * (params.ahtChats || 90)) / concurrency;
const workingMinutes = (params.fteHours || 189) * 60 * (params.utilization || 0.86);
results.chats = {
fte: Math.ceil(totalMinutes / workingMinutes * 10) / 10,
totalHours: (totalMinutes / 60).toFixed(1),
concurrency
};
}
// Итого
results.total = {
fteInSchedule: Object.values(results).reduce((sum, r) => sum + (r.fte || 0), 0),
fteWithAbsence: 0,
absence: params.plannedAbsence || 0.1,
unplannedAbsence: params.unplannedAbsence || 0.05
};
// Учёт отсутствий
const absenceMultiplier = 1 / (1 - (results.total.absence + results.total.unplannedAbsence));
results.total.fteWithAbsence = Math.ceil(results.total.fteInSchedule * absenceMultiplier * 10) / 10;
return results;
}
http.createServer(async (req, res) => {
// CORS для предпросмотра
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(200);
return res.end();
}
// API: расчёт WFM
if (req.method === "POST" && req.url === "/api/calculate") {
const params = await body(req);
const results = calculateFTE(params);
// Сохранение расчёта
const data = readData();
data.calculations.push({
id: Date.now(),
date: new Date().toISOString(),
params,
results
});
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(results));
}
// API: история расчётов
if (req.method === "GET" && req.url === "/api/history") {
const data = readData();
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(data.calculations.slice(-10).reverse()));
}
// Статика
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("WFM Calculator на порту " + PORT));