Files
magistr/frontend/react/teacher/AbsencePanel.jsx
dipatrik10 97bf3cc50a feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента
полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md).

- 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой
- Удалены legacy-views и переходный слой LegacyTabView (45 файлов)
- Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки
- CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf)
- Фиксы: lazy-импорт именованных экспортов (краш после авторизации),
  растяжение #admin-root/#settings-root на всю ширину,
  возврат subjects/lessonTypes из справочников (краш просмотра расписаний),
  отказ от заведомо широкого поиска расписания без выбранной цели,
  центрирование карточки логина
- Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину
- Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
2026-09-28 23:15:04 +03:00

159 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { formatIsoDate, toIsoDate } from '../shared/dates.js';
import {
absenceStatusClass,
absenceStatusLabel,
cancelAbsence,
createAbsence,
fetchAbsences,
validateAbsence
} from './teacher-service.js';
export function AbsencePanel() {
const [formOpen, setFormOpen] = useState(false);
const [startDate, setStartDate] = useState(() => toIsoDate(new Date()));
const [endDate, setEndDate] = useState(() => toIsoDate(new Date()));
const [reason, setReason] = useState('');
const [status, setStatus] = useState({ message: '', isError: false });
const [items, setItems] = useState(null);
const [error, setError] = useState(false);
useEffect(() => {
let active = true;
(async () => {
try {
const loaded = await fetchAbsences();
if (active) setItems(loaded);
} catch {
if (active) setError(true);
}
})();
return () => {
active = false;
};
}, []);
async function reload() {
try {
setItems(await fetchAbsences());
setError(false);
} catch {
setError(true);
}
}
async function submit(event) {
event.preventDefault();
const validation = validateAbsence({ endDate, reason, startDate });
if (validation) {
setStatus({ message: validation, isError: true });
return;
}
try {
await createAbsence({ endDate, reason: reason.trim(), startDate });
setReason('');
setStatus({ message: 'Заявка отправлена кафедре', isError: false });
await reload();
} catch {
setStatus({ message: 'Не удалось отправить заявку об отсутствии', isError: true });
}
}
async function cancel(id) {
try {
await cancelAbsence(id);
setStatus({ message: 'Заявка отменена', isError: false });
await reload();
} catch {
setStatus({ message: 'Не удалось отменить заявку', isError: true });
}
}
return (
<section className="teacher-absence-panel">
<div className="teacher-absence-heading">
<div>
<span>Изменение планов</span>
<h2>Сообщить об отсутствии</h2>
<p>Заявка поступит кафедре на подтверждение. После согласования учебный отдел сможет подобрать замены.</p>
</div>
<button
className="btn btn-md btn-secondary"
type="button"
aria-expanded={formOpen}
onClick={() => setFormOpen(open => !open)}
>
{formOpen ? 'Скрыть форму' : 'Новая заявка'}
</button>
</div>
<form className="teacher-absence-form" hidden={!formOpen} onSubmit={submit}>
<label>
<span>Начало</span>
<input
className="date-input"
type="date"
required
value={startDate}
onChange={event => setStartDate(event.target.value)}
/>
</label>
<label>
<span>Окончание</span>
<input
className="date-input"
type="date"
required
value={endDate}
onChange={event => setEndDate(event.target.value)}
/>
</label>
<label className="teacher-absence-reason">
<span>Причина</span>
<input
type="text"
maxLength={2000}
placeholder="Например, командировка"
required
value={reason}
onChange={event => setReason(event.target.value)}
/>
</label>
<button className="btn btn-md btn-primary" type="submit">Отправить кафедре</button>
</form>
<div className={`teacher-absence-status${status.isError ? ' error' : ''}${status.message && !status.isError ? ' success' : ''}`}>
{status.message}
</div>
<div className="teacher-absence-list" aria-live="polite">
{error ? (
<div className="teacher-absence-empty error">Не удалось загрузить заявки об отсутствии</div>
) : items === null ? (
<div className="teacher-absence-empty">Загрузка заявок...</div>
) : items.length === 0 ? (
<div className="teacher-absence-empty">Заявок об отсутствии пока нет</div>
) : items.map(item => (
<article key={item.id} className="teacher-absence-item">
<span className={`teacher-absence-marker ${absenceStatusClass(item.status)}`} />
<div>
<strong>{formatIsoDate(item.startDate)} — {formatIsoDate(item.endDate)}</strong>
<p>{item.reason || 'Причина не указана'}</p>
</div>
<span className={`teacher-absence-badge ${absenceStatusClass(item.status)}`}>
{absenceStatusLabel(item.status)}
</span>
{item.status === 'PENDING' && (
<button
className="btn btn-sm btn-danger-subtle"
type="button"
onClick={() => void cancel(item.id)}
>
Отменить
</button>
)}
</article>
))}
</div>
</section>
);
}