// Состояние приложения
const state = {
user: null,
currentPage: null,
currentCourse: null,
currentLesson: null,
userData: null,
allData: null,
notificationsOpen: false
};
// API утилиты
const api = {
async post(url, data) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return res.json();
},
async get(url) {
const res = await fetch(url);
return res.json();
},
async put(url, data) {
const res = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return res.json();
}
};
// Утилиты
function formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function formatDateTime(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
}
function getCourseStatus(assignment) {
const today = new Date().toISOString().split('T')[0];
if (assignment.status === 'completed') return 'completed';
if (assignment.dueDate < today) return 'overdue';
const progress = state.userData?.progress?.filter(p => {
const a = state.userData.assignments.find(x => x.id === p.assignmentId);
return a?.id === assignment.id;
}).length || 0;
return progress > 0 ? 'in_progress' : 'not_started';
}
function getCourseProgress(assignment) {
const course = state.allData?.courses?.find(c => c.id === assignment.courseId);
if (!course) return 0;
const totalLessons = course.modules.reduce((sum, m) => sum + m.lessons.length, 0);
if (totalLessons === 0) return 0;
const completed = state.userData?.progress?.filter(p => {
const a = state.userData.assignments.find(x => x.id === p.assignmentId);
return a?.id === assignment.id && p.completed;
}).length || 0;
return Math.round((completed / totalLessons) * 100);
}
// Рендеринг страниц
function render() {
const app = document.getElementById('app');
if (!state.user) {
renderLoginPage(app);
return;
}
switch (state.currentPage) {
case 'dashboard':
renderDashboard(app);
break;
case 'courses':
renderCourses(app);
break;
case 'course':
renderCourseDetail(app);
break;
case 'lesson':
renderLesson(app);
break;
case 'quiz':
renderQuiz(app);
break;
case 'certificates':
renderCertificates(app);
break;
case 'manager':
renderManagerDashboard(app);
break;
case 'admin':
renderAdminDashboard(app);
break;
case 'admin-courses':
renderAdminCourses(app);
break;
case 'admin-users':
renderAdminUsers(app);
break;
case 'admin-assignments':
renderAdminAssignments(app);
break;
case 'admin-reports':
renderAdminReports(app);
break;
case 'admin-course-builder':
renderAdminCourseBuilder(app);
break;
case 'admin-assignment-create':
renderAdminAssignmentCreate(app);
break;
default:
renderDashboard(app);
}
}
// Страница входа
function renderLoginPage(container) {
container.innerHTML = `
🎓 Академия компании
Платформа обучения и развития сотрудников
Тестовые аккаунты:
Сотрудник: employee / employee123
Руководитель: manager / manager123
Администратор: admin / admin123
`;
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const login = document.getElementById('login').value;
const password = document.getElementById('password').value;
const errorEl = document.getElementById('loginError');
try {
const result = await api.post('/api/login', { login, password });
if (result.ok) {
state.user = result.user;
state.currentPage = 'dashboard';
await loadUserData();
render();
} else {
errorEl.textContent = result.error || 'Неверный логин или пароль';
errorEl.classList.add('show');
}
} catch (err) {
errorEl.textContent = 'Ошибка подключения к серверу';
errorEl.classList.add('show');
}
});
}
function togglePassword() {
const input = document.getElementById('password');
input.type = input.type === 'password' ? 'text' : 'password';
}
// Загрузка данных пользователя
async function loadUserData() {
if (state.user.role === 'admin') {
const allData = await api.get('/api/admin/data');
state.allData = allData.data;
} else {
const userData = await api.get(`/api/user/${state.user.id}`);
state.userData = userData.data;
}
}
// Layout с боковым меню
function renderLayout(container, content) {
const roleNames = { employee: 'Сотрудник', manager: 'Руководитель', admin: 'Администратор' };
const unreadCount = state.userData?.notifications?.filter(n => !n.read).length || 0;
let adminMenu = '';
if (state.user.role === 'admin') {
adminMenu = `
`;
}
container.innerHTML = `
${getPageTitle()}
${content}
`;
if (state.user.role !== 'admin') {
renderNotifications();
}
}
function getPageTitle() {
const titles = {
dashboard: 'Главная',
courses: 'Мои курсы',
course: 'Курс',
lesson: 'Урок',
quiz: 'Тестирование',
certificates: 'Сертификаты',
manager: 'Кабинет руководителя',
admin: 'Панель администратора',
'admin-courses': 'Управление курсами',
'admin-users': 'Сотрудники',
'admin-assignments': 'Назначения',
'admin-reports': 'Отчёты',
'admin-course-builder': 'Конструктор курса',
'admin-assignment-create': 'Назначение курса'
};
return titles[state.currentPage] || 'Академия';
}
function navigate(page, params = {}) {
state.currentPage = page;
state.currentCourse = params.course || null;
state.currentLesson = params.lesson || null;
state.notificationsOpen = false;
// Закрываем мобильное меню
document.getElementById('sidebar')?.classList.remove('open');
render();
window.scrollTo(0, 0);
}
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
}
function toggleNotifications() {
state.notificationsOpen = !state.notificationsOpen;
if (state.notificationsOpen) {
renderNotifications();
}
render();
}
function renderNotifications() {
const panel = document.getElementById('notificationsPanel');
if (!panel) return;
const list = document.getElementById('notificationsList');
if (!state.userData?.notifications?.length) {
list.innerHTML = '';
return;
}
list.innerHTML = state.userData.notifications
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.map(n => `
${n.title}
${n.message}
${formatDateTime(n.createdAt)}
`).join('');
}
async function markNotificationRead(id) {
await api.put(`/api/notifications/${id}`, { read: true });
await loadUserData();
renderNotifications();
}
function logout() {
state.user = null;
state.userData = null;
state.currentPage = null;
render();
}
// Дашборд сотрудника
function renderDashboard(container) {
const assignments = state.userData?.assignments || [];
const statusCounts = {
not_started: assignments.filter(a => getCourseStatus(a) === 'not_started').length,
in_progress: assignments.filter(a => getCourseStatus(a) === 'in_progress').length,
completed: assignments.filter(a => getCourseStatus(a) === 'completed').length,
overdue: assignments.filter(a => getCourseStatus(a) === 'overdue').length
};
const totalProgress = assignments.length > 0
? Math.round(assignments.reduce((sum, a) => sum + getCourseProgress(a), 0) / assignments.length)
: 0;
const upcomingDeadlines = assignments
.filter(a => getCourseStatus(a) !== 'completed' && getCourseStatus(a) !== 'overdue')
.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate))
.slice(0, 3);
const content = `
${assignments.length}
Всего курсов
${statusCounts.completed}
Завершено
${statusCounts.in_progress}
В процессе
${statusCounts.overdue}
Просрочено
${upcomingDeadlines.length > 0 ? `
${upcomingDeadlines.map(a => {
const course = state.allData?.courses?.find(c => c.id === a.courseId) || state.userData?.courses?.find(c => c.id === a.courseId);
return `
${course?.title || 'Курс'}
До ${formatDate(a.dueDate)}
`;
}).join('')}
` : ''}
${assignments.slice(0, 3).map(a => renderCourseCard(a)).join('')}
`;
renderLayout(container, content);
}
function renderCourseCard(assignment) {
const course = state.allData?.courses?.find(c => c.id === assignment.courseId);
if (!course) return '';
const status = getCourseStatus(assignment);
const progress = getCourseProgress(assignment);
const category = state.allData?.categories?.find(c => c.id === course.categoryId);
const statusLabels = {
not_started: 'Не начат',
in_progress: 'В процессе',
completed: 'Завершён',
overdue: 'Просрочен'
};
return `
${category ? `
${category.name}` : ''}
${statusLabels[status]}
${course.title}
${course.description}
⏱ ${course.duration} ч
📚 ${course.modules.reduce((sum, m) => sum + m.lessons.length, 0)} уроков
${status !== 'not_started' ? `
` : ''}
`;
}
// Каталог курсов
function renderCourses(container) {
const assignments = state.userData?.assignments || [];
const [searchTerm, setSearchTerm] = state.courseSearch || ['', null];
const [statusFilter, setStatusFilter] = state.courseStatusFilter || ['all', null];
const [categoryFilter, setCategoryFilter] = state.courseCategoryFilter || ['all', null];
const filtered = assignments.filter(a => {
const course = state.allData?.courses?.find(c => c.id === a.courseId);
if (!course) return false;
const matchesSearch = course.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
course.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || getCourseStatus(a) === statusFilter;
const matchesCategory = categoryFilter === 'all' || course.categoryId === parseInt(categoryFilter);
return matchesSearch && matchesStatus && matchesCategory;
});
const content = `
${filtered.length > 0 ? filtered.map(a => renderCourseCard(a)).join('') : `
📚
Курсы не найдены
Попробуйте изменить параметры поиска
`}
`;
renderLayout(container, content);
}
// Страница курса
function renderCourseDetail(container) {
const course = state.allData?.courses?.find(c => c.id === state.currentCourse);
const assignment = state.userData?.assignments?.find(a => a.courseId === course?.id);
if (!course || !assignment) {
container.innerHTML = '';
return;
}
const status = getCourseStatus(assignment);
const progress = getCourseProgress(assignment);
const category = state.allData?.categories?.find(c => c.id === course.categoryId);
const allLessons = course.modules.flatMap(m => m.lessons);
const completedLessons = state.userData?.progress?.filter(p => {
const a = state.userData.assignments.find(x => x.id === p.assignmentId);
return a?.id === assignment.id && p.completed;
}).length || 0;
const nextLesson = allLessons.find((l, i) => {
const prevLessons = allLessons.slice(0, i);
return !prevLessons.every(pl => {
const p = state.userData?.progress?.find(prog => {
const a = state.userData.assignments.find(x => x.id === prog.assignmentId);
return a?.id === assignment.id && prog.lessonId === pl.id;
});
return p?.completed;
});
});
const content = `
Содержание курса
${course.modules.map((module, mi) => `
${module.lessons.map(l => {
const isCompleted = state.userData?.progress?.some(p => {
const a = state.userData.assignments.find(x => x.id === p.assignmentId);
return a?.id === assignment.id && p.lessonId === l.id && p.completed;
});
const icon = l.type === 'video' ? '🎥' : l.type === 'quiz' ? '📝' : '📄';
return `
-
${icon}
${l.title}
${isCompleted ? ' ✓' : ''}
`;
}).join('')}
`).join('')}
`;
renderLayout(container, content);
}
// Страница урока
function renderLesson(container) {
const course = state.allData?.courses?.find(c => c.id === state.currentCourse);
const assignment = state.userData?.assignments?.find(a => a.courseId === course?.id);
const lesson = course?.modules.flatMap(m => m.lessons).find(l => l.id === state.currentLesson);
if (!course || !assignment || !lesson) {
container.innerHTML = '';
return;
}
const allLessons = course.modules.flatMap(m => m.lessons);
const currentIndex = allLessons.findIndex(l => l.id === lesson.id);
const prevLesson = allLessons[currentIndex - 1];
const nextLesson = allLessons[currentIndex + 1];
const isCompleted = state.userData?.progress?.some(p => {
const a = state.userData.assignments.find(x => x.id === p.assignmentId);
return a?.id === assignment.id && p.lessonId === lesson.id && p.completed;
});
let lessonContent = '';
if (lesson.type === 'video') {
lessonContent = `
Длительность: ${lesson.duration} мин
`;
} else if (lesson.type === 'quiz') {
lessonContent = `
`;
} else {
lessonContent = `${lesson.content}
`;
}
const content = `
${course.title}
${currentIndex + 1} / ${allLessons.length}
${lesson.title}
${lessonContent}
${!isCompleted && lesson.type !== 'quiz' ? `
` : ''}
`;
renderLayout(container, content);
}
async function markLessonComplete(assignmentId, lessonId) {
await api.post('/api/progress', { assignmentId, lessonId });
await loadUserData();
render();
}
// Тестирование
function renderQuiz(container) {
const course = state.allData?.courses?.find(c => c.id === state.currentCourse);
const assignment = state.userData?.assignments?.find(a => a.courseId === course?.id);
const lesson = course?.modules.flatMap(m => m.lessons).find(l => l.id === state.currentLesson);
if (!course || !assignment || !lesson) {
container.innerHTML = '';
return;
}
const [quizState, setQuizState] = state.currentQuiz || [{
currentQuestion: 0,
answers: [],
completed: false,
result: null
}];
if (quizState.completed && quizState.result) {
const isPassed = quizState.result.passed;
const content = `
${quizState.result.score}%
${isPassed ? '✓ Тест пройден!' : '✕ Тест не пройден'}
Правильных ответов: ${quizState.result.correct} из ${lesson.questions.length}
${!isPassed && course.maxAttempts > 1 ? `
` : ''}
${isPassed ? `
` : ''}
`;
renderLayout(container, content);
return;
}
const question = lesson.questions[quizState.currentQuestion];
if (!question) {
container.innerHTML = '';
return;
}
const content = `
${quizState.currentQuestion < lesson.questions.length - 1 ? `
` : `
`}
`;
renderLayout(container, content);
}
function selectQuizAnswer(questionIndex, answer) {
const quizState = state.currentQuiz || [{ currentQuestion: 0, answers: [], completed: false }];
quizState[0].answers[questionIndex] = answer;
state.currentQuiz = [quizState[0]];
render();
}
function selectQuizAnswerMultiple(questionIndex, optionIndex, isChecked) {
const quizState = state.currentQuiz || [{ currentQuestion: 0, answers: [], completed: false }];
if (!quizState[0].answers[questionIndex]) {
quizState[0].answers[questionIndex] = [];
}
if (isChecked) {
quizState[0].answers[questionIndex].push(optionIndex);
} else {
quizState[0].answers[questionIndex] = quizState[0].answers[questionIndex].filter(i => i !== optionIndex);
}
state.currentQuiz = [quizState[0]];
render();
}
function prevQuestion() {
const quizState = state.currentQuiz || [{ currentQuestion: 0, answers: [], completed: false }];
if (quizState[0].currentQuestion > 0) {
quizState[0].currentQuestion--;
state.currentQuiz = [quizState[0]];
render();
}
}
function nextQuestion() {
const quizState = state.currentQuiz || [{ currentQuestion: 0, answers: [], completed: false }];
if (quizState[0].currentQuestion < state.allData.courses.find(c => c.id === state.currentCourse).modules.flatMap(m => m.lessons).find(l => l.id === state.currentLesson).questions.length - 1) {
quizState[0].currentQuestion++;
state.currentQuiz = [quizState[0]];
render();
}
}
async function submitQuiz(assignmentId, lessonId) {
const quizState = state.currentQuiz || [{ answers: [] }];
const answers = quizState[0].answers.map((answer, i) => ({
questionId: i + 1,
answer
}));
const result = await api.post('/api/quiz', { assignmentId, lessonId, answers });
if (result.ok) {
quizState[0].completed = true;
quizState[0].result = { score: result.score, passed: result.passed, correct: Math.round(result.score / 100 * state.allData.courses.find(c => c.id === state.currentCourse).modules.flatMap(m => m.lessons).find(l => l.id === lessonId).questions.length) };
state.currentQuiz = [quizState[0]];
render();
}
}
function restartQuiz() {
state.currentQuiz = [{ currentQuestion: 0, answers: [], completed: false }];
render();
}
// Сертификаты
function renderCertificates(container) {
const certificates = state.userData?.certificates || [];
const content = `
${certificates.length > 0 ? `
${certificates.map(cert => {
const course = state.allData?.courses?.find(c => c.id === cert.courseId);
return `
🏆
${course?.title || 'Курс'}
Выдан ${formatDate(cert.issueDate)}
Действителен до ${formatDate(cert.validUntil)}
`;
}).join('')}
` : `
🏆
Нет сертификатов
Завершите курсы чтобы получить сертификаты
`}
`;
renderLayout(container, content);
}
function viewCertificate(id) {
const cert = state.userData.certificates.find(c => c.id === id);
const course = state.allData.courses.find(c => c.id === cert.courseId);
const user = state.userData.user;
const certWindow = window.open('', '_blank');
certWindow.document.write(`
Сертификат - ${user.name}
🎓
СЕРТИФИКАТ
Настоящим подтверждается, что
${user.name}
успешно завершил курс
${course?.title || 'Курс'}
${formatDate(cert.issueDate)}
${cert.certificateNumber}
`);
certWindow.document.close();
}
// Кабинет руководителя
async function renderManagerDashboard(container) {
const managerData = await api.get(`/api/manager/${state.user.id}`);
const data = managerData.data;
if (!data) {
container.innerHTML = 'Нет данных по сотрудникам
';
return;
}
const content = `
${data.totalEmployees}
Сотрудников
${data.totalAssignments}
Назначено курсов
${data.completed}
Завершено
${data.overdue}
Просрочено
| ФИО |
Должность |
Назначено |
Завершено |
В процессе |
Просрочено |
Действия |
${data.employees.map(emp => `
| ${emp.name} |
${emp.position} |
${emp.assignmentsCount} |
${emp.completedCount} |
${emp.inProgressCount} |
${emp.overdueCount} |
|
`).join('')}
`;
renderLayout(container, content);
}
// Панель администратора
async function renderAdminDashboard(container) {
const dashboard = await api.get('/api/admin/dashboard');
const data = dashboard.data;
const content = `
${data.totalUsers}
Сотрудников
${data.totalCourses}
Курсов
${data.completionRate}%
Завершение
${data.overdueAssignments}
Просрочено
${data.avgScore}%
Средний балл
`;
renderLayout(container, content);
}
function renderAdminCourses(container) {
const courses = state.allData?.courses || [];
const content = `
Все курсы
| Название |
Категория |
Автор |
Длительность |
Уроков |
Статус |
Действия |
${courses.map(c => {
const category = state.allData.categories.find(cat => cat.id === c.categoryId);
const lessonsCount = c.modules.reduce((sum, m) => sum + m.lessons.length, 0);
return `
| ${c.title} |
${category?.name || 'Без категории'} |
${c.author} |
${c.duration} ч |
${lessonsCount} |
${c.status === 'published' ? 'Опубликован' : 'Черновик'} |
|
`;
}).join('')}
`;
renderLayout(container, content);
}
function renderAdminUsers(container) {
const users = state.allData?.users || [];
const content = `
Сотрудники
| ФИО |
Роль |
Подразделение |
Должность |
Логин |
Действия |
${users.map(u => `
| ${u.name} |
${u.role === 'admin' ? 'Админ' : u.role === 'manager' ? 'Руководитель' : 'Сотрудник'} |
${u.department} |
${u.position} |
${u.login} |
|
`).join('')}
`;
renderLayout(container, content);
}
function renderAdminAssignments(container) {
const assignments = state.allData?.assignments || [];
const content = `
Назначения курсов
| Курс |
Сотрудник |
Дата начала |
Срок |
Статус |
Обязательно |
${assignments.slice(0, 20).map(a => {
const course = state.allData.courses.find(c => c.id === a.courseId);
const user = state.allData.users.find(u => u.id === a.userId);
return `
| ${course?.title || 'Курс'} |
${user?.name || 'Сотрудник'} |
${formatDate(a.startDate)} |
${formatDate(a.dueDate)} |
${a.status} |
${a.mandatory ? '✓' : ''} |
`;
}).join('')}
`;
renderLayout(container, content);
}
function renderAdminReports(container) {
const content = `
`;
renderLayout(container, content);
}
let currentReportData = null;
function generateReport() {
const type = document.getElementById('reportType').value;
const from = document.getElementById('reportFrom').value;
const to = document.getElementById('reportTo').value;
let data = [];
let columns = [];
if (type === 'employees') {
columns = ['Сотрудник', 'Подразделение', 'Должность', 'Назначено', 'Завершено', 'В процессе', 'Просрочено'];
data = state.allData.users.filter(u => u.role === 'employee').map(u => {
const assignments = state.allData.assignments.filter(a => a.userId === u.id);
return {
employee: u.name,
department: u.department,
position: u.position,
assigned: 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
};
});
} else if (type === 'departments') {
columns = ['Подразделение', 'Сотрудников', 'Назначений', 'Завершено', 'Просрочено', '% завершения'];
data = state.allData.departments.map(d => {
const deptUsers = state.allData.users.filter(u => u.department === d.name && u.role === 'employee');
const assignments = state.allData.assignments.filter(a => deptUsers.some(u => u.id === a.userId));
const completed = assignments.filter(a => a.status === 'completed').length;
return {
department: d.name,
employees: deptUsers.length,
assigned: assignments.length,
completed,
overdue: assignments.filter(a => a.status === 'overdue').length,
completionRate: assignments.length > 0 ? Math.round((completed / assignments.length) * 100) : 0
};
});
} else if (type === 'courses') {
columns = ['Курс', 'Категория', 'Назначений', 'Завершено', 'В процессе', 'Просрочено', 'Ср. балл'];
data = state.allData.courses.map(c => {
const assignments = state.allData.assignments.filter(a => a.courseId === c.id);
const attempts = state.allData.quizAttempts.filter(q => {
const assignment = state.allData.assignments.find(a => a.id === q.assignmentId);
return assignment && assignment.courseId === c.id;
});
const avgScore = attempts.length > 0 ? Math.round(attempts.reduce((sum, q) => sum + q.score, 0) / attempts.length) : 0;
return {
course: c.title,
category: state.allData.categories.find(cat => cat.id === c.categoryId)?.name || '',
assigned: 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,
avgScore
};
});
} else if (type === 'tests') {
columns = ['Сотрудник', 'Курс', 'Дата', 'Балл', 'Результат', 'Попытка'];
const quizAttempts = state.allData.quizAttempts.map(q => {
const assignment = state.allData.assignments.find(a => a.id === q.assignmentId);
const user = state.allData.users.find(u => u.id === assignment?.userId);
const course = state.allData.courses.find(c => c.id === assignment?.courseId);
return {
employee: user?.name || '',
course: course?.title || '',
date: formatDateTime(q.attemptAt),
score: q.score,
passed: q.passed ? 'Пройден' : 'Не пройден',
attempt: q.id
};
});
data = quizAttempts;
} else if (type === 'overdue') {
columns = ['Сотрудник', 'Курс', 'Дата назначения', 'Срок', 'Дней просрочки'];
const today = new Date();
data = state.allData.assignments.filter(a => a.status === 'overdue').map(a => {
const user = state.allData.users.find(u => u.id === a.userId);
const course = state.allData.courses.find(c => c.id === a.courseId);
const dueDate = new Date(a.dueDate);
const daysOverdue = Math.floor((today - dueDate) / (1000 * 60 * 60 * 24));
return {
employee: user?.name || '',
course: course?.title || '',
assigned: formatDate(a.startDate),
due: formatDate(a.dueDate),
daysOverdue
};
});
}
currentReportData = { data, columns };
const resultDiv = document.getElementById('reportResult');
if (data.length === 0) {
resultDiv.innerHTML = 'Нет данных за выбранный период
';
return;
}
resultDiv.innerHTML = `
${columns.map(c => `| ${c} | `).join('')}
${data.map(row => `
${columns.map(c => `| ${row[c.toLowerCase()] || row[c] || ''} | `).join('')}
`).join('')}
`;
}
function exportReport() {
if (!currentReportData || currentReportData.data.length === 0) {
alert('Сначала сформируйте отчёт');
return;
}
const { data, columns } = currentReportData;
const csvData = data.map(row => {
const csvRow = {};
columns.forEach(col => {
const key = col.toLowerCase();
csvRow[col] = row[key] || row[col] || '';
});
return csvRow;
});
const filename = `otchet_${new Date().toISOString().split('T')[0]}.csv`;
exportReportCSV(csvData, filename);
}
// Конструктор курсов
function renderAdminCourseBuilder(container) {
const courseId = state.editCourseId;
const course = courseId ? state.allData.courses.find(c => c.id === courseId) : null;
const content = `
${course ? 'Редактирование курса' : 'Создание курса'}
${course?.modules.map((module, mi) => `
${module.lessons.map((lesson, li) => `
${lesson.type === 'video' ? '🎥' : lesson.type === 'quiz' ? '📝' : '📄'}
${lesson.title}
${lesson.duration} мин
`).join('')}
`).join('') || '
Модули не добавлены
'}
`;
renderLayout(container, content);
}
// Создание назначения
function renderAdminAssignmentCreate(container) {
const content = `
Назначение курса
`;
renderLayout(container, content);
}
function toggleAssignTarget() {
const type = document.getElementById('assignType').value;
document.getElementById('usersSelect').classList.toggle('hidden', type === 'department');
document.getElementById('departmentSelect').classList.toggle('hidden', type === 'users');
}
async function createAssignment() {
const courseId = parseInt(document.getElementById('assignCourse').value);
if (!courseId) {
alert('Выберите курс');
return;
}
const assignType = document.getElementById('assignType').value;
let userIds = [];
if (assignType === 'users') {
userIds = Array.from(document.querySelectorAll('.user-checkbox:checked')).map(cb => parseInt(cb.value));
} else {
const deptId = parseInt(document.getElementById('assignDepartment').value);
userIds = state.allData.users
.filter(u => u.department === state.allData.departments.find(d => d.id === deptId)?.name)
.map(u => u.id);
}
if (userIds.length === 0) {
alert('Выберите хотя бы одного сотрудника');
return;
}
const startDate = document.getElementById('assignStartDate').value;
const dueDate = document.getElementById('assignDueDate').value;
const mandatory = document.getElementById('assignMandatory').checked;
const result = await api.post('/api/assignments', {
courseId,
userIds,
assignedBy: state.user.id,
startDate,
dueDate,
mandatory
});
if (result.ok) {
alert(`Курс назначен ${userIds.length} сотрудникам(а)`);
navigate('admin-assignments');
} else {
alert('Ошибка при назначении');
}
}
function addModule() {
alert('Функция добавления модуля в разработке - используйте API напрямую');
}
function addLesson(moduleIndex) {
alert('Функция добавления урока в разработке - используйте API напрямую');
}
function removeModule(index) {
if (confirm('Удалить этот модуль со всеми уроками?')) {
// Логика удаления
}
}
function removeLesson(moduleIndex, lessonIndex) {
if (confirm('Удалить этот урок?')) {
// Логика удаления
}
}
function editLesson(moduleIndex, lessonIndex) {
alert('Редактирование урока в разработке');
}
async function saveCourseDraft() {
const title = document.getElementById('courseTitle').value;
const description = document.getElementById('courseDescription').value;
const categoryId = parseInt(document.getElementById('courseCategory').value);
const author = document.getElementById('courseAuthor').value;
const duration = parseInt(document.getElementById('courseDuration').value);
const passScore = parseInt(document.getElementById('coursePassScore').value);
const maxAttempts = parseInt(document.getElementById('courseMaxAttempts').value);
const cover = document.getElementById('courseCover').value;
const sequential = document.getElementById('courseSequential').checked;
const certificateEnabled = document.getElementById('courseCertificate').checked;
if (!title || !description) {
alert('Заполните название и описание');
return;
}
const result = await api.post('/api/courses', {
title,
description,
categoryId,
author,
duration,
passScore,
maxAttempts,
cover,
sequential,
certificateEnabled,
modules: []
});
if (result.ok) {
alert('Черновик сохранён');
state.editCourseId = result.course.id;
navigate('admin-courses');
}
}
async function publishCourse() {
if (!state.editCourseId) {
alert('Сначала сохраните курс как черновик');
return;
}
const result = await api.put(`/api/courses/${state.editCourseId}`, {
status: 'published'
});
if (result.ok) {
alert('Курс опубликован!');
navigate('admin-courses');
}
}
// Экспорт отчётов в CSV
function exportReportCSV(data, filename) {
if (!data || data.length === 0) {
alert('Нет данных для экспорта');
return;
}
const headers = Object.keys(data[0]);
const csv = [
headers.join(','),
...data.map(row => headers.map(h => `"${String(row[h] || '').replace(/"/g, '""')}"`).join(','))
].join('\n');
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
// Инициализация
render();