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
This commit is contained in:
158
frontend/react/teacher/AbsencePanel.jsx
Normal file
158
frontend/react/teacher/AbsencePanel.jsx
Normal file
@@ -0,0 +1,158 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user