import { useEffect, useState } from 'react'; import { formatIsoDate, toIsoDate } from '../shared/dates.js'; import { activeDatePreferences, activeWeekPreferences, createPreference, deletePreference, fetchPreferenceMeta, fetchPreferences, findCompactPreference, findWeekPreference, preferenceStatusLabel, preferenceTypeClass, preferenceTypeLabel, preferenceTypeSymbol, resolveCurrentSemester, resolveSelectedSemester, semesterLabel } from './teacher-service.js'; const WEEK_DAY_LABELS = Object.freeze(['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']); export function PreferencesTab() { const [meta, setMeta] = useState({ semesters: [], timeSlots: [] }); const [metaError, setMetaError] = useState(false); const [semesterId, setSemesterId] = useState(''); const [preferences, setPreferences] = useState([]); const [brush, setBrush] = useState('SOFT_PREFERRED'); const [message, setMessage] = useState({ text: '', isError: false }); const [dateValue, setDateValue] = useState(''); const [dateComment, setDateComment] = useState(''); const [busyCell, setBusyCell] = useState(''); useEffect(() => { let active = true; (async () => { try { const loaded = await fetchPreferenceMeta(); if (!active) return; setMeta(loaded); const semesters = loaded.semesters || []; const current = resolveCurrentSemester(semesters, toIsoDate(new Date())); if (current) setSemesterId(String(current.id)); } catch { if (active) setMetaError(true); } })(); return () => { active = false; }; }, []); useEffect(() => { if (!semesterId) return undefined; let active = true; (async () => { try { const loaded = await fetchPreferences(semesterId); if (!active) return; setPreferences(loaded); setMessage({ text: '', isError: false }); } catch { if (active) setMessage({ text: 'Не удалось загрузить пожелания', isError: true }); } })(); return () => { active = false; }; }, [semesterId]); const semester = resolveSelectedSemester(meta.semesters, semesterId); useEffect(() => { const min = semester?.startDate || ''; const max = semester?.endDate || ''; if (!dateValue || dateValue < min || dateValue > max) { setDateValue(min); } }, [semester, dateValue]); async function reloadPreferences() { try { setPreferences(await fetchPreferences(semesterId)); } catch { setMessage({ text: 'Не удалось загрузить пожелания', isError: true }); } } function showMessage(text, isError) { setMessage({ text, isError }); } async function handleCell(dayOfWeek, timeSlotId) { const existing = findWeekPreference(preferences, dayOfWeek, timeSlotId); if (existing) { if (existing.status === 'APPROVED') { showMessage('Согласованную отметку может снять кафедра или учебный отдел', true); return; } try { await deletePreference(existing.id); showMessage('Ожидающее пожелание отменено', false); await reloadPreferences(); } catch { showMessage('Не удалось отменить пожелание', true); } return; } const cellKey = `${dayOfWeek}:${timeSlotId}`; setBusyCell(cellKey); try { await createPreference({ dayOfWeek, preferenceType: brush, semesterId: Number(semesterId), timeSlotId: Number(timeSlotId) }); showMessage('Отметка отправлена на согласование', false); await reloadPreferences(); } catch { showMessage('Не удалось сохранить пожелание', true); } finally { setBusyCell(''); } } async function submitDate(event) { event.preventDefault(); if (!dateValue) return; try { await createPreference({ comment: dateComment.trim() || null, preferenceDate: dateValue, preferenceType: 'HARD_UNAVAILABLE', semesterId: Number(semesterId) }); setDateComment(''); showMessage('Дата недоступности отправлена на согласование', false); await reloadPreferences(); } catch { showMessage('Не удалось сохранить пожелание', true); } } async function cancelDatePreference(id) { try { await deletePreference(id); showMessage('Ожидающее пожелание отменено', false); await reloadPreferences(); } catch { showMessage('Не удалось отменить пожелание', true); } } async function toggleCompact(type, checked, existingId) { try { if (!checked && existingId) { await deletePreference(existingId); } else if (checked) { await createPreference({ preferenceType: type, semesterId: Number(semesterId) }); } await reloadPreferences(); } catch { showMessage('Не удалось сохранить пожелание', true); await reloadPreferences(); } } const slots = meta.timeSlots || []; const weekCells = activeWeekPreferences(preferences); const dateItems = activeDatePreferences(preferences); return ( <>
Контур доступности

Соберите ритм семестра до составления расписания

Строгая недоступность блокирует назначение. Мягкие отметки и пожелания к компактности помогают учебному отделу выбрать удобный вариант.

Недельный шаблон

Отмечайте интервалы одним касанием

Предпочтительно Нежелательно Строгий запрет Ожидает согласования
{metaError ? (
Не удалось загрузить параметры семестра
) : slots.length === 0 ? (
Базовая сетка времени не настроена
) : ( <>
Пара
{WEEK_DAY_LABELS.map(day => (
{day}
))} {slots.flatMap(slot => [
{slot.orderNumber} {String(slot.startTime || '').slice(0, 5)}–{String(slot.endTime || '').slice(0, 5)}
, ...WEEK_DAY_LABELS.map((_, index) => { const dayOfWeek = index + 1; const item = findWeekPreference(weekCells, dayOfWeek, slot.id); const title = item ? `${preferenceTypeLabel(item.preferenceType)} · ${preferenceStatusLabel(item.status)}` : 'Добавить отметку'; return ( ); }) ])} )}
{message.text}
01

Полная недоступность

Добавьте отдельную дату внутри семестра — все пары этого дня будут считаться строгим ограничением.

setDateValue(event.target.value)} /> setDateComment(event.target.value)} />
{dateItems.length === 0 ? (
Даты полной недоступности не добавлены
) : dateItems.map(item => (
{formatIsoDate(item.preferenceDate)} {item.comment || 'Весь день'}
{preferenceStatusLabel(item.status)} {item.status === 'PENDING' && ( )}
))}
02

Компактность недели

Эти мягкие пожелания не отменяют занятия автоматически, но видны при составлении правил.

); } function CompactToggle({ type, title, description, preference, onToggle }) { const approved = preference?.status === 'APPROVED'; return ( ); }