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));