import { clearAuthState, fetchWithAuth, logoutSession, restoreSession } from '../auth-session.js'; (async () => { const session = await restoreSession(); if (!session || session.role !== 'TEACHER') { window.location.replace('/'); return; } const teacherId = session.userId; const grid = document.getElementById('schedule-grid'); const statusLine = document.getElementById('status-line'); const weekLabel = document.getElementById('week-label'); const datePicker = document.getElementById('date-picker'); const absenceForm = document.getElementById('teacher-absence-form'); const absenceList = document.getElementById('teacher-absence-list'); const absenceStatus = document.getElementById('teacher-absence-status'); const absenceStart = document.getElementById('teacher-absence-start'); const absenceEnd = document.getElementById('teacher-absence-end'); const absenceReason = document.getElementById('teacher-absence-reason'); let weekStart = startOfWeek(new Date()); document.getElementById('logout-link').addEventListener('click', async (event) => { event.preventDefault(); await logoutSession(); window.location.replace('/'); }); document.getElementById('theme-toggle-local').addEventListener('click', () => { const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; document.documentElement.dataset.theme = next; localStorage.setItem('theme', next); }); document.documentElement.dataset.theme = localStorage.getItem('theme') || 'light'; document.getElementById('prev-week').addEventListener('click', () => moveWeek(-1)); document.getElementById('today-week').addEventListener('click', () => { weekStart = startOfWeek(new Date()); loadSchedule(); }); document.getElementById('next-week').addEventListener('click', () => moveWeek(1)); datePicker.addEventListener('change', () => { if (datePicker.value) { weekStart = startOfWeek(new Date(datePicker.value)); loadSchedule(); } }); document.getElementById('absence-form-toggle').addEventListener('click', event => { const expanded = event.currentTarget.getAttribute('aria-expanded') === 'true'; event.currentTarget.setAttribute('aria-expanded', String(!expanded)); event.currentTarget.textContent = expanded ? 'Новая заявка' : 'Скрыть форму'; absenceForm.hidden = expanded; }); absenceForm.addEventListener('submit', submitAbsence); absenceList.addEventListener('click', cancelAbsence); const todayIso = toIsoDate(new Date()); absenceStart.value = todayIso; absenceEnd.value = todayIso; loadSchedule(); loadAbsences(); async function loadAbsences() { absenceList.innerHTML = '
Загрузка заявок...
'; try { const items = await fetchJson('/api/teacher-absences'); absenceList.innerHTML = items.length ? items.map(item => `
${escapeHtml(formatIsoDate(item.startDate))} — ${escapeHtml(formatIsoDate(item.endDate))}

${escapeHtml(item.reason || 'Причина не указана')}

${escapeHtml(absenceStatusLabel(item.status))} ${item.status === 'PENDING' ? `` : ''}
`).join('') : '
Заявок об отсутствии пока нет
'; } catch (error) { absenceList.innerHTML = `
${escapeHtml(error.message)}
`; } } async function submitAbsence(event) { event.preventDefault(); showAbsenceStatus('', false); if (!absenceStart.value || !absenceEnd.value || !absenceReason.value.trim()) { showAbsenceStatus('Заполните период и причину отсутствия', true); return; } if (absenceEnd.value < absenceStart.value) { showAbsenceStatus('Дата окончания не может быть раньше даты начала', true); return; } try { await fetchJson('/api/teacher-absences', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startDate: absenceStart.value, endDate: absenceEnd.value, reason: absenceReason.value.trim() }) }); absenceReason.value = ''; showAbsenceStatus('Заявка отправлена кафедре', false); await loadAbsences(); } catch (error) { showAbsenceStatus(error.message, true); } } async function cancelAbsence(event) { const button = event.target.closest('[data-cancel-absence]'); if (!button) return; button.disabled = true; try { await fetchJson(`/api/teacher-absences/${encodeURIComponent(button.dataset.cancelAbsence)}`, { method: 'DELETE' }); showAbsenceStatus('Заявка отменена', false); await loadAbsences(); } catch (error) { showAbsenceStatus(error.message, true); button.disabled = false; } } function showAbsenceStatus(message, isError) { absenceStatus.textContent = message || ''; absenceStatus.classList.toggle('error', Boolean(isError)); absenceStatus.classList.toggle('success', Boolean(message) && !isError); } async function loadSchedule() { const startDate = toIsoDate(weekStart); const endDate = toIsoDate(addDays(weekStart, 6)); datePicker.value = startDate; weekLabel.textContent = `${formatDate(weekStart)} - ${formatDate(addDays(weekStart, 6))}`; if (!teacherId) { renderEmptyWeek('Не удалось определить преподавателя'); showStatus('Выполните вход заново, чтобы получить идентификатор пользователя', true); return; } renderEmptyWeek('Загрузка...'); try { const lessons = await fetchJson(`/api/schedule?teacherId=${encodeURIComponent(teacherId)}&startDate=${startDate}&endDate=${endDate}`); renderWeek(lessons); showStatus(lessons.length ? `Найдено занятий: ${lessons.length}` : 'На этой неделе занятий нет', false); } catch (error) { renderEmptyWeek('Ошибка загрузки'); showStatus(error.message, true); } } function renderWeek(lessons) { if (window.matchMedia('(max-width: 980px)').matches) { renderMobileWeek(lessons); return; } const slots = collectSlots(lessons); const lessonsByKey = new Map(); lessons.forEach(lesson => { lessonsByKey.set(`${lesson.date}:${lesson.timeSlotOrder}`, lesson); }); grid.innerHTML = [ '
', ...daysOfWeek().map(date => renderHead(date)), ...slots.flatMap(slot => [ `
${escapeHtml(slot.label)}
`, ...daysOfWeek().map(date => renderCell(lessonsByKey.get(`${toIsoDate(date)}:${slot.order}`))) ]) ].join(''); } function renderMobileWeek(lessons) { const lessonsByDate = new Map(); lessons.forEach(lesson => { if (!lessonsByDate.has(lesson.date)) lessonsByDate.set(lesson.date, []); lessonsByDate.get(lesson.date).push(lesson); }); grid.innerHTML = daysOfWeek().map(date => { const key = toIsoDate(date); const dayLessons = lessonsByDate.get(key) || []; return `
${renderHead(date)}
${dayLessons.length ? dayLessons.map(renderLesson).join('') : '
Занятий нет
'}
`; }).join(''); } function renderEmptyWeek(message) { if (window.matchMedia('(max-width: 980px)').matches) { grid.innerHTML = daysOfWeek().map(date => `
${renderHead(date)}
${escapeHtml(message)}
`).join(''); return; } grid.innerHTML = [ '
', ...daysOfWeek().map(date => renderHead(date)), `
Неделя
`, ...daysOfWeek().map(() => `
${escapeHtml(message)}
`) ].join(''); } function renderHead(date) { return `
${escapeHtml(dayName(date))} ${escapeHtml(formatDate(date))}
`; } function renderCell(lesson) { return `
${lesson ? renderLesson(lesson) : '
Нет занятия
'}
`; } function renderLesson(lesson) { const groups = Array.isArray(lesson.groupNames) ? lesson.groupNames.join(', ') : ''; const subgroups = Array.isArray(lesson.subgroupNames) && lesson.subgroupNames.length ? lesson.subgroupNames.join(', ') : lesson.subgroupName; return `
${escapeHtml(lesson.subjectName)}
${escapeHtml(trimTime(lesson.startTime))} - ${escapeHtml(trimTime(lesson.endTime))} ${escapeHtml(lesson.lessonTypeName)} ${subgroups ? `${escapeHtml(subgroups)}` : ''} ${escapeHtml(lesson.classroomName)} ${groups ? `${escapeHtml(groups)}` : ''}
`; } function collectSlots(lessons) { const slots = new Map(); lessons.forEach(lesson => { slots.set(lesson.timeSlotOrder, { order: lesson.timeSlotOrder, label: `${trimTime(lesson.startTime)} - ${trimTime(lesson.endTime)}` }); }); if (!slots.size) { slots.set(1, { order: 1, label: 'Расписание' }); } return [...slots.values()].sort((a, b) => a.order - b.order); } function moveWeek(delta) { weekStart = addDays(weekStart, delta * 7); loadSchedule(); } function daysOfWeek() { return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index)); } async function fetchJson(url, options = {}) { const response = await fetchWithAuth(url, options); const data = await response.json().catch(() => null); if (response.status === 401) { clearAuthState(); window.location.replace('/'); throw new Error('Требуется повторный вход в систему'); } if (!response.ok) { throw new Error(data?.message || `Ошибка HTTP: ${response.status}`); } return data; } function showStatus(message, isError) { statusLine.textContent = message; statusLine.classList.toggle('error', isError); } function startOfWeek(date) { const copy = new Date(date.getFullYear(), date.getMonth(), date.getDate()); const day = copy.getDay() || 7; copy.setDate(copy.getDate() - day + 1); return copy; } function addDays(date, days) { const copy = new Date(date); copy.setDate(copy.getDate() + days); return copy; } function toIsoDate(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } function formatDate(date) { return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' }); } function formatIsoDate(value) { const [year, month, day] = String(value || '').split('-'); return year && month && day ? `${day}.${month}.${year}` : '—'; } function absenceStatusLabel(status) { return { PENDING: 'На подтверждении', APPROVED: 'Согласовано', REJECTED: 'Отклонено', RESOLVED: 'Замены выполнены', CANCELLED: 'Отменено' }[status] || status || '—'; } function absenceStatusClass(status) { return `is-${String(status || '').toLowerCase()}`; } function dayName(date) { const value = date.toLocaleDateString('ru-RU', { weekday: 'long' }); return value.charAt(0).toUpperCase() + value.slice(1); } function trimTime(value) { return String(value || '').slice(0, 5); } function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } })();