const http = require("http");
const fs = require("fs");
const path = require("path");
const formidable = require("formidable");
const PORT = process.env.PORT || 3000;
const DATA_FILE = path.join(__dirname, "data.json");
// Инициализация данных
function initData() {
const defaultData = {
users: [
{ id: 1, login: "admin", password: "admin123", name: "Администратор Системы", role: "admin", department: "Административный отдел", position: "Администратор" },
{ id: 2, login: "manager", password: "manager123", name: "Иванов Петр Сергеевич", role: "manager", department: "Департамент информационных технологий", position: "Руководитель департамента" },
{ id: 3, login: "manager2", password: "manager123", name: "Сидорова Анна Павловна", role: "manager", department: "Отдел продаж", position: "Руководитель отдела" },
{ id: 4, login: "employee", password: "employee123", name: "Алексеев Дмитрий Михайлович", role: "employee", department: "Департамент информационных технологий", position: "Ведущий разработчик" },
{ id: 5, login: "employee2", password: "employee123", name: "Борисова Елена Владимировна", role: "employee", department: "Департамент информационных технологий", position: "Системный администратор" },
{ id: 6, login: "employee3", password: "employee123", name: "Васильев Игорь Александрович", role: "employee", department: "Отдел информационной безопасности", position: "Специалист по ИБ" },
{ id: 7, login: "employee4", password: "employee123", name: "Григорьева Ольга Николаевна", role: "employee", department: "Отдел продаж", position: "Менеджер по продажам" },
{ id: 8, login: "employee5", password: "employee123", name: "Дмитриев Максим Андреевич", role: "employee", department: "Отдел продаж", position: "Старший менеджер" },
{ id: 9, login: "employee6", password: "employee123", name: "Егорова Татьяна Сергеевна", role: "employee", department: "Служба технической поддержки", position: "Инженер поддержки" },
{ id: 10, login: "employee7", password: "employee123", name: "Жуков Андрей Викторович", role: "employee", department: "Служба технической поддержки", position: "Старший инженер" },
{ id: 11, login: "employee8", password: "employee123", name: "Зайцева Мария Игоревна", role: "employee", department: "Административный отдел", position: "HR-менеджер" },
{ id: 12, login: "employee9", password: "employee123", name: "Иванов Сергей Петрович", role: "employee", department: "Департамент информационных технологий", position: "Разработчик" },
{ id: 13, login: "employee10", password: "employee123", name: "Козлова Анна Дмитриевна", role: "employee", department: "Отдел информационной безопасности", position: "Аналитик ИБ" },
{ id: 14, login: "employee11", password: "employee123", name: "Кузнецов Владимир Игоревич", role: "employee", department: "Отдел продаж", position: "Менеджер по работе с клиентами" },
{ id: 15, login: "employee12", password: "employee123", name: "Лебедева Екатерина Александровна", role: "employee", department: "Служба технической поддержки", position: "Специалист поддержки" },
{ id: 16, login: "employee13", password: "employee123", name: "Морозов Дмитрий Евгеньевич", role: "employee", department: "Департамент информационных технологий", position: "DevOps-инженер" },
{ id: 17, login: "employee14", password: "employee123", name: "Новикова Ирина Владимировна", role: "employee", department: "Отдел продаж", position: "Торговый представитель" },
{ id: 18, login: "employee15", password: "employee123", name: "Орлов Алексей Николаевич", role: "employee", department: "Департамент информационных технологий", position: "Тестировщик" },
{ id: 19, login: "employee16", password: "employee123", name: "Павлова Светлана Андреевна", role: "employee", department: "Административный отдел", position: "Делопроизводитель" },
{ id: 20, login: "employee17", password: "employee123", name: "Петров Михаил Сергеевич", role: "employee", department: "Служба технической поддержки", position: "Руководитель группы" },
{ id: 21, login: "employee18", password: "employee123", name: "Смирнова Ольга Петровна", role: "employee", department: "Отдел информационной безопасности", position: "Ведущий специалист" },
{ id: 22, login: "employee19", password: "employee123", name: "Соколов Иван Дмитриевич", role: "employee", department: "Департамент информационных технологий", position: "Архитектор ПО" },
{ id: 23, login: "employee20", password: "employee123", name: "Федорова Наталья Игоревна", role: "employee", department: "Отдел продаж", position: "Ключевой менеджер" },
{ id: 24, login: "employee21", password: "employee123", name: "Чернышов Андрей Александрович", role: "employee", department: "Служба технической поддержки", position: "Инженер" },
{ id: 25, login: "employee22", password: "employee123", name: "Шарова Елена Михайловна", role: "employee", department: "Административный отдел", position: "Офис-менеджер" },
{ id: 26, login: "employee23", password: "employee123", name: "Щербаков Дмитрий Викторович", role: "employee", department: "Департамент информационных технологий", position: "Аналитик данных" },
{ id: 27, login: "employee24", password: "employee123", name: "Яковлева Мария Сергеевна", role: "employee", department: "Отдел информационной безопасности", position: "Специалист по сертификации" },
{ id: 28, login: "employee25", password: "employee123", name: "Волков Сергей Николаевич", role: "employee", department: "Отдел продаж", position: "Региональный менеджер" }
],
departments: [
{ id: 1, name: "Департамент информационных технологий", head: "Иванов Петр Сергеевич" },
{ id: 2, name: "Служба технической поддержки", head: "Петров Михаил Сергеевич" },
{ id: 3, name: "Отдел информационной безопасности", head: "Смирнова Ольга Петровна" },
{ id: 4, name: "Отдел продаж", head: "Сидорова Анна Павловна" },
{ id: 5, name: "Административный отдел", head: "Лебедева Екатерина Александровна" }
],
categories: [
{ id: 1, name: "Информационная безопасность", color: "#dc3545" },
{ id: 2, name: "Охрана труда", color: "#fd7e14" },
{ id: 3, name: "Техническое обучение", color: "#0d6efd" },
{ id: 4, name: "Работа с клиентами", color: "#198754" },
{ id: 5, name: "Внутренние процессы", color: "#6f42c1" },
{ id: 6, name: "Развитие руководителей", color: "#d63384" }
],
courses: [
{
id: 1,
title: "Основы информационной безопасности",
description: "Изучите основные принципы защиты информации, работы с конфиденциальными данными и предотвращения утечек.",
categoryId: 1,
author: "Смирнова Ольга Петровна",
duration: 8,
cover: "https://picsum.photos/seed/security/400/225",
status: "published",
sequential: true,
modules: [
{
id: 1,
title: "Введение в информационную безопасность",
lessons: [
{ id: 1, title: "Что такое информационная безопасность", type: "video", duration: 15, content: "https://www.youtube.com/embed/dQw4w9WgXcQ", order: 1 },
{ id: 2, title: "Классификация угроз", type: "text", duration: 20, content: "
Основные виды угроз
Угрозы информационной безопасности делятся на несколько категорий:
- Внешние угрозы (хакеры, вирусы)
- Внутренние угрозы (ошибки сотрудников)
- Технические угрозы (сбои оборудования)
", order: 2 },
{ id: 3, title: "Тест по модулю 1", type: "quiz", duration: 10, questions: [
{ id: 1, type: "single", text: "Что из перечисленного относится к внешним угрозам?", options: ["Ошибка сотрудника", "Хакерская атака", "Сбой питания", "Все вышеперечисленное"], correct: 1 },
{ id: 2, type: "truefalse", text: "Вирусы являются только внешней угрозой", correct: false }
], order: 3 }
]
},
{
id: 2,
title: "Защита персональных данных",
lessons: [
{ id: 4, title: "Понятие персональных данных", type: "text", duration: 25, content: "Персональные данные (ПДн)
Любая информация, относящаяся к прямо или косвенно определенному физическому лицу.
", order: 1 },
{ id: 5, title: "Требования к защите ПДн", type: "video", duration: 18, content: "https://www.youtube.com/embed/dQw4w9WgXcQ", order: 2 },
{ id: 6, title: "Тест по защите ПДн", type: "quiz", duration: 15, questions: [
{ id: 1, type: "single", text: "Что является персональными данными?", options: ["ФИО", "Телефон", "Email", "Все вышеперечисленное"], correct: 3 },
{ id: 2, type: "multiple", text: "Какие меры защиты ПДн обязательны?", options: ["Шифрование", "Разграничение доступа", "Резервное копирование", "Все вышеперечисленное"], correct: [0, 1, 2, 3] }
], order: 3 }
]
}
],
passScore: 80,
maxAttempts: 3,
certificateEnabled: true
},
{
id: 2,
title: "Охрана труда и техника безопасности",
description: "Правила безопасной работы в офисе и на производстве. Действия в чрезвычайных ситуациях.",
categoryId: 2,
author: "Жуков Андрей Викторович",
duration: 6,
cover: "https://picsum.photos/seed/safety/400/225",
status: "published",
sequential: false,
modules: [
{
id: 1,
title: "Общие требования охраны труда",
lessons: [
{ id: 1, title: "Вводный инструктаж", type: "text", duration: 30, content: "Вводный инструктаж проводят со всеми новыми сотрудниками
Программа включает: общие сведения о предприятии, правила внутреннего распорядка, основные опасные факторы.
", order: 1 },
{ id: 2, title: "Пожарная безопасность", type: "video", duration: 20, content: "https://www.youtube.com/embed/dQw4w9WgXcQ", order: 2 }
]
}
],
passScore: 75,
maxAttempts: 5,
certificateEnabled: true
},
{
id: 3,
title: "Работа с корпоративными системами",
description: "Навыки работы с внутренними системами компании: CRM, ERP, система документооборота.",
categoryId: 3,
author: "Алексеев Дмитрий Михайлович",
duration: 10,
cover: "https://picsum.photos/seed/systems/400/225",
status: "published",
sequential: true,
modules: [
{
id: 1,
title: "Корпоративная почта и календарь",
lessons: [
{ id: 1, title: "Работа с почтой", type: "text", duration: 15, content: "Основные принципы деловой переписки
", order: 1 }
]
}
],
passScore: 70,
maxAttempts: 3,
certificateEnabled: false
},
{
id: 4,
title: "Основы клиентского сервиса",
description: "Стандарты обслуживания клиентов, техники работы с возражениями, разрешение конфликтных ситуаций.",
categoryId: 4,
author: "Сидорова Анна Павловна",
duration: 12,
cover: "https://picsum.photos/seed/service/400/225",
status: "published",
sequential: false,
modules: [
{
id: 1,
title: "Стандарты сервиса",
lessons: [
{ id: 1, title: "Пять шагов обслуживания", type: "text", duration: 20, content: "Пять шагов идеального обслуживания
", order: 1 }
]
}
],
passScore: 85,
maxAttempts: 3,
certificateEnabled: true
},
{
id: 5,
title: "Управление инцидентами",
description: "Методология ITIL, классификация инцидентов, приоритизация и эскалация.",
categoryId: 3,
author: "Борисова Елена Владимировна",
duration: 8,
cover: "https://picsum.photos/seed/incidents/400/225",
status: "published",
sequential: true,
modules: [
{
id: 1,
title: "Основы ITIL",
lessons: [
{ id: 1, title: "Что такое инцидент", type: "text", duration: 15, content: "Инцидент
Нарушение нормального функционирования услуги.
", order: 1 }
]
}
],
passScore: 75,
maxAttempts: 3,
certificateEnabled: true
},
{
id: 6,
title: "Защита персональных данных",
description: "152-ФЗ, требования регуляторов, практические меры защиты ПДн в компании.",
categoryId: 1,
author: "Васильев Игорь Александрович",
duration: 6,
cover: "https://picsum.photos/seed/pd/400/225",
status: "published",
sequential: false,
modules: [
{
id: 1,
title: "Законодательство о ПДн",
lessons: [
{ id: 1, title: "152-ФЗ overview", type: "text", duration: 25, content: "Федеральный закон № 152-ФЗ
", order: 1 }
]
}
],
passScore: 90,
maxAttempts: 2,
certificateEnabled: true
},
{
id: 7,
title: "Введение для новых сотрудников",
description: "Онбординг: история компании, ценности, организационная структура, правила внутреннего распорядка.",
categoryId: 5,
author: "Зайцева Мария Игоревна",
duration: 4,
cover: "https://picsum.photos/seed/onboarding/400/225",
status: "published",
sequential: false,
modules: [
{
id: 1,
title: "О компании",
lessons: [
{ id: 1, title: "История и миссия", type: "video", duration: 10, content: "https://www.youtube.com/embed/dQw4w9WgXcQ", order: 1 }
]
}
],
passScore: 60,
maxAttempts: 5,
certificateEnabled: true
},
{
id: 8,
title: "Управление командой",
description: "Навыки лидерства, мотивация сотрудников, делегирование, проведение эффективных встреч.",
categoryId: 6,
author: "Иванов Петр Сергеевич",
duration: 16,
cover: "https://picsum.photos/seed/leadership/400/225",
status: "published",
sequential: true,
modules: [
{
id: 1,
title: "Лидерство",
lessons: [
{ id: 1, title: "Стили лидерства", type: "text", duration: 30, content: "Основные стили лидерства
", order: 1 }
]
}
],
passScore: 80,
maxAttempts: 3,
certificateEnabled: true
},
{
id: 9,
title: "Деловая переписка",
description: "Правила написания деловых писем, этикет электронной почты, шаблоны документов.",
categoryId: 5,
author: "Павлова Светлана Андреевна",
duration: 5,
cover: "https://picsum.photos/seed/email/400/225",
status: "published",
sequential: false,
modules: [
{
id: 1,
title: "Основы деловой переписки",
lessons: [
{ id: 1, title: "Структура письма", type: "text", duration: 20, content: "Структура делового письма
", order: 1 }
]
}
],
passScore: 70,
maxAttempts: 3,
certificateEnabled: false
},
{
id: 10,
title: "Первая помощь",
description: "Базовые навыки оказания первой помощи: СЛР,止血, помощь при ожогах и травмах.",
categoryId: 2,
author: "Лебедева Екатерина Александровна",
duration: 8,
cover: "https://picsum.photos/seed/firstaid/400/225",
status: "published",
sequential: false,
modules: [
{
id: 1,
title: "Основы СЛР",
lessons: [
{ id: 1, title: "Сердечно-лёгочная реанимация", type: "video", duration: 15, content: "https://www.youtube.com/embed/dQw4w9WgXcQ", order: 1 }
]
}
],
passScore: 85,
maxAttempts: 3,
certificateEnabled: true
}
],
assignments: [],
progress: [],
quizAttempts: [],
certificates: [],
notifications: [],
nextIds: {
userId: 29,
departmentId: 6,
courseId: 11,
assignmentId: 1,
progressId: 1,
quizAttemptId: 1,
certificateId: 1,
notificationId: 1
}
};
// Создаём назначения по умолчанию
const now = new Date();
const pastDate = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
const futureDate1 = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
const futureDate2 = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
const futureDate3 = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
// Назначения для сотрудника (employee)
defaultData.assignments.push(
{ id: 1, courseId: 1, userId: 4, assignedBy: 2, startDate: pastDate.toISOString().split('T')[0], dueDate: futureDate1.toISOString().split('T')[0], status: "in_progress", mandatory: true },
{ id: 2, courseId: 2, userId: 4, assignedBy: 2, startDate: now.toISOString().split('T')[0], dueDate: futureDate2.toISOString().split('T')[0], status: "not_started", mandatory: true },
{ id: 3, courseId: 7, userId: 4, assignedBy: 1, startDate: pastDate.toISOString().split('T')[0], dueDate: pastDate.toISOString().split('T')[0], status: "overdue", mandatory: true },
{ id: 4, courseId: 4, userId: 4, assignedBy: 2, startDate: now.toISOString().split('T')[0], dueDate: futureDate3.toISOString().split('T')[0], status: "completed", mandatory: false }
);
// Назначения для других сотрудников
for (let i = 5; i <= 28; i++) {
const courseIds = [1, 2, 7];
const randomCourse = courseIds[Math.floor(Math.random() * courseIds.length)];
const status = ["not_started", "in_progress", "completed", "overdue"][Math.floor(Math.random() * 4)];
let dueDate = futureDate1;
if (status === "overdue") dueDate = pastDate;
if (status === "completed") dueDate = pastDate;
defaultData.assignments.push({
id: defaultData.assignments.length + 1,
courseId: randomCourse,
userId: i,
assignedBy: 2,
startDate: now.toISOString().split('T')[0],
dueDate: dueDate.toISOString().split('T')[0],
status: status,
mandatory: Math.random() > 0.5
});
}
// Прогресс по урокам
defaultData.progress.push(
{ id: 1, assignmentId: 1, lessonId: 1, completed: true, completedAt: new Date().toISOString() },
{ id: 2, assignmentId: 1, lessonId: 2, completed: true, completedAt: new Date().toISOString() },
{ id: 3, assignmentId: 4, lessonId: 1, completed: true, completedAt: new Date().toISOString() }
);
// Попытки тестов
defaultData.quizAttempts.push(
{ id: 1, assignmentId: 1, lessonId: 3, score: 100, passed: true, answers: [{ questionId: 1, answer: 1 }, { questionId: 2, answer: true }], attemptAt: new Date().toISOString() },
{ id: 2, assignmentId: 4, lessonId: 1, score: 85, passed: true, answers: [], attemptAt: new Date().toISOString() }
);
// Сертификаты
defaultData.certificates.push({
id: 1,
userId: 4,
courseId: 4,
issueDate: new Date().toISOString().split('T')[0],
certificateNumber: "CERT-2024-001",
validUntil: new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
});
// Уведомления
defaultData.notifications = [
{ id: 1, userId: 4, title: "Новый курс назначен", message: "Вам назначен курс 'Основы клиентского сервиса'", type: "assignment", read: false, createdAt: new Date().toISOString() },
{ id: 2, userId: 4, title: "Срок приближается", message: "До завершения курса 'Основы информационной безопасности' осталось 3 дня", type: "reminder", read: false, createdAt: new Date().toISOString() },
{ id: 3, userId: 4, title: "Сертификат получен", message: "Вы успешно завершили курс 'Основы клиентского сервиса'", type: "certificate", read: true, createdAt: new Date().toISOString() }
];
if (!fs.existsSync(DATA_FILE)) {
fs.writeFileSync(DATA_FILE, JSON.stringify(defaultData, null, 2));
}
return defaultData;
}
function loadData() {
try {
return JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
} catch (_) {
return initData();
}
}
function saveData(data) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
}
function parseBody(req) {
return new Promise((resolve) => {
let body = "";
req.on("data", chunk => body += chunk);
req.on("end", () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch (_) {
resolve({});
}
});
});
}
function jsonRes(res, status, data) {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
// API Роуты
async function handleApi(req, res, data) {
const url = req.url.split("?")[0];
const method = req.method;
// Авторизация
if (method === "POST" && url === "/api/login") {
const body = await parseBody(req);
const user = data.users.find(u => u.login === body.login && u.password === body.password);
if (user) {
return jsonRes(res, 200, { ok: true, user: { id: user.id, login: user.login, name: user.name, role: user.role } });
}
return jsonRes(res, 401, { ok: false, error: "Неверный логин или пароль" });
}
// Получение данных пользователя
if (method === "GET" && url.startsWith("/api/user/")) {
const userId = parseInt(url.split("/")[3]);
const user = data.users.find(u => u.id === userId);
if (!user) return jsonRes(res, 404, { ok: false, error: "Пользователь не найден" });
const userData = {
user,
assignments: data.assignments.filter(a => a.userId === userId),
progress: data.progress.filter(p => {
const assignment = data.assignments.find(a => a.id === p.assignmentId);
return assignment && assignment.userId === userId;
}),
quizAttempts: data.quizAttempts.filter(q => {
const assignment = data.assignments.find(a => a.id === q.assignmentId);
return assignment && assignment.userId === userId;
}),
certificates: data.certificates.filter(c => c.userId === userId),
notifications: data.notifications.filter(n => n.userId === userId)
};
return jsonRes(res, 200, { ok: true, data: userData });
}
// Обновление прогресса
if (method === "POST" && url === "/api/progress") {
const body = await parseBody(req);
const existing = data.progress.find(p => p.assignmentId === body.assignmentId && p.lessonId === body.lessonId);
if (!existing) {
data.progress.push({
id: data.nextIds.progressId++,
assignmentId: body.assignmentId,
lessonId: body.lessonId,
completed: true,
completedAt: new Date().toISOString()
});
}
saveData(data);
return jsonRes(res, 200, { ok: true });
}
// Сохранение попытки теста
if (method === "POST" && url === "/api/quiz") {
const body = await parseBody(req);
const lesson = data.courses.flatMap(c => c.modules.flatMap(m => m.lessons)).find(l => l.id === body.lessonId);
const correctCount = body.answers.reduce((acc, ans) => {
const q = lesson.questions.find(q => q.id === ans.questionId);
if (!q) return acc;
if (q.type === "single") return acc + (q.correct === ans.answer ? 1 : 0);
if (q.type === "truefalse") return acc + (q.correct === ans.answer ? 1 : 0);
if (q.type === "multiple") {
const correctSet = new Set(q.correct);
const answerSet = new Set(ans.answer);
const intersection = [...correctSet].filter(x => answerSet.has(x)).length;
return acc + (intersection === correctSet.size ? 1 : 0);
}
return acc;
}, 0);
const totalQuestions = lesson.questions.length;
const score = Math.round((correctCount / totalQuestions) * 100);
const passed = score >= (lesson.passScore || data.courses.find(c => c.modules.some(m => m.lessons.some(l => l.id === body.lessonId)))?.passScore || 70);
data.quizAttempts.push({
id: data.nextIds.quizAttemptId++,
assignmentId: body.assignmentId,
lessonId: body.lessonId,
score,
passed,
answers: body.answers,
attemptAt: new Date().toISOString()
});
// Обновляем статус назначения если все уроки пройдены
const assignment = data.assignments.find(a => a.id === body.assignmentId);
if (assignment && passed) {
const course = data.courses.find(c => c.id === assignment.courseId);
const allLessons = course.modules.flatMap(m => m.lessons);
const completedLessons = data.progress.filter(p => p.assignmentId === body.assignmentId && p.completed).length + 1;
if (completedLessons >= allLessons.length) {
assignment.status = "completed";
if (course.certificateEnabled) {
data.certificates.push({
id: data.nextIds.certificateId++,
userId: assignment.userId,
courseId: assignment.courseId,
issueDate: new Date().toISOString().split('T')[0],
certificateNumber: `CERT-${new Date().getFullYear()}-${String(data.certificates.length + 1).padStart(3, '0')}`,
validUntil: new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
});
}
}
}
saveData(data);
return jsonRes(res, 200, { ok: true, score, passed });
}
// Получение всех данных для администратора
if (method === "GET" && url === "/api/admin/data") {
return jsonRes(res, 200, { ok: true, data });
}
// Создание курса
if (method === "POST" && url === "/api/courses") {
const body = await parseBody(req);
const newCourse = {
id: data.nextIds.courseId++,
...body,
status: "draft",
modules: body.modules || []
};
data.courses.push(newCourse);
saveData(data);
return jsonRes(res, 200, { ok: true, course: newCourse });
}
// Обновление курса
if (method === "PUT" && url.startsWith("/api/courses/")) {
const courseId = parseInt(url.split("/")[3]);
const courseIndex = data.courses.findIndex(c => c.id === courseId);
if (courseIndex === -1) return jsonRes(res, 404, { ok: false, error: "Курс не найден" });
const body = await parseBody(req);
data.courses[courseIndex] = { ...data.courses[courseIndex], ...body };
saveData(data);
return jsonRes(res, 200, { ok: true, course: data.courses[courseIndex] });
}
// Назначение курса
if (method === "POST" && url === "/api/assignments") {
const body = await parseBody(req);
const newAssignments = [];
body.userIds.forEach(userId => {
newAssignments.push({
id: data.nextIds.assignmentId++,
courseId: body.courseId,
userId,
assignedBy: body.assignedBy,
startDate: body.startDate,
dueDate: body.dueDate,
status: "not_started",
mandatory: body.mandatory
});
});
data.assignments.push(...newAssignments);
saveData(data);
return jsonRes(res, 200, { ok: true, assignments: newAssignments });
}
// Отметка уведомления как прочитанного
if (method === "PUT" && url.startsWith("/api/notifications/")) {
const notifId = parseInt(url.split("/")[3]);
const notification = data.notifications.find(n => n.id === notifId);
if (notification) {
notification.read = true;
saveData(data);
}
return jsonRes(res, 200, { ok: true });
}
// Статистика для руководителя
if (method === "GET" && url.startsWith("/api/manager/")) {
const managerId = parseInt(url.split("/")[3]);
const manager = data.users.find(u => u.id === managerId);
if (!manager) return jsonRes(res, 404, { ok: false, error: "Пользователь не найден" });
const department = manager.department;
const subordinates = data.users.filter(u => u.department === department && u.role === "employee");
const subordinateIds = subordinates.map(s => s.id);
const assignments = data.assignments.filter(a => subordinateIds.includes(a.userId));
const stats = {
totalEmployees: subordinates.length,
totalAssignments: assignments.length,
completed: assignments.filter(a => a.status === "completed").length,
inProgress: assignments.filter(a => a.status === "in_progress").length,
overdue: assignments.filter(a => a.status === "overdue").length,
employees: subordinates.map(e => {
const empAssignments = assignments.filter(a => a.userId === e.id);
return {
...e,
assignmentsCount: empAssignments.length,
completedCount: empAssignments.filter(a => a.status === "completed").length,
inProgressCount: empAssignments.filter(a => a.status === "in_progress").length,
overdueCount: empAssignments.filter(a => a.status === "overdue").length
};
})
};
return jsonRes(res, 200, { ok: true, data: stats });
}
// Дашборд администратора
if (method === "GET" && url === "/api/admin/dashboard") {
const totalUsers = data.users.filter(u => u.role === "employee").length;
const totalCourses = data.courses.filter(c => c.status === "published").length;
const totalAssignments = data.assignments.length;
const completedAssignments = data.assignments.filter(a => a.status === "completed").length;
const overdueAssignments = data.assignments.filter(a => a.status === "overdue").length;
const avgScore = data.quizAttempts.length > 0
? Math.round(data.quizAttempts.reduce((sum, q) => sum + q.score, 0) / data.quizAttempts.length)
: 0;
return jsonRes(res, 200, {
ok: true,
data: {
totalUsers,
totalCourses,
completionRate: totalAssignments > 0 ? Math.round((completedAssignments / totalAssignments) * 100) : 0,
overdueAssignments,
avgScore,
totalAssignments
}
});
}
jsonRes(res, 404, { ok: false, error: "API endpoint not found" });
}
// Статика
function handleStatic(req, res) {
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" });
res.end(fs.readFileSync(full));
return;
}
res.writeHead(404);
res.end("Not found");
}
// Инициализация и сервер
initData();
http.createServer(async (req, res) => {
if (req.url.startsWith("/api/")) {
const data = loadData();
await handleApi(req, res, data);
} else {
handleStatic(req, res);
}
}).listen(PORT, () => console.log("Сервер запущен на порту " + PORT));