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)}
`).join('');
return;
}
grid.innerHTML = [
'',
...daysOfWeek().map(date => renderHead(date)),
`Неделя
`,
...daysOfWeek().map(() => ``)
].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("'", ''');
}
})();