sozday-sovremennyy-adaptivny/server.js

685 lines
36 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 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: "<h3>Основные виды угроз</h3><p>Угрозы информационной безопасности делятся на несколько категорий:</p><ul><li>Внешние угрозы (хакеры, вирусы)</li><li>Внутренние угрозы (ошибки сотрудников)</li><li>Технические угрозы (сбои оборудования)</li></ul>", 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: "<h3>Персональные данные (ПДн)</h3><p>Любая информация, относящаяся к прямо или косвенно определенному физическому лицу.</p>", 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: "<h3>Вводный инструктаж проводят со всеми новыми сотрудниками</h3><p>Программа включает: общие сведения о предприятии, правила внутреннего распорядка, основные опасные факторы.</p>", 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: "<h3>Основные принципы деловой переписки</h3>", 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: "<h3>Пять шагов идеального обслуживания</h3>", 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: "<h3>Инцидент</h3><p>Нарушение нормального функционирования услуги.</p>", 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: "<h3>Федеральный закон № 152-ФЗ</h3>", 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: "<h3>Основные стили лидерства</h3>", 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: "<h3>Структура делового письма</h3>", 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));