685 lines
36 KiB
JavaScript
685 lines
36 KiB
JavaScript
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));
|