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:
dipatrik10
2026-09-28 23:15:04 +03:00
parent c6083b35e0
commit 97bf3cc50a
167 changed files with 31283 additions and 19033 deletions

View File

@@ -0,0 +1,149 @@
import { useEffect, useState } from 'react';
import { ConfirmDialog } from '../shared/ui/ConfirmDialog.jsx';
import {
createEducationForm,
deleteEducationForm,
fetchEducationForms
} from './settings-service.js';
export function EduFormsTab() {
const [forms, setForms] = useState(null);
const [loadError, setLoadError] = useState(false);
const [name, setName] = useState('');
const [alert, setAlert] = useState({ text: '', isError: false });
const [deleting, setDeleting] = useState(null);
useEffect(() => {
let active = true;
(async () => {
try {
const loaded = await fetchEducationForms();
if (!active) return;
setForms(loaded);
setLoadError(false);
} catch {
if (active) setLoadError(true);
}
})();
return () => {
active = false;
};
}, []);
async function reload() {
try {
setForms(await fetchEducationForms());
setLoadError(false);
} catch {
setLoadError(true);
}
}
async function submit(event) {
event.preventDefault();
const trimmed = name.trim();
if (!trimmed) {
setAlert({ text: 'Введите название', isError: true });
return;
}
try {
const created = await createEducationForm(trimmed);
setAlert({ text: `Форма «${created.name}» создана`, isError: false });
setName('');
await reload();
} catch (error) {
setAlert({ text: error.message || 'Ошибка создания', isError: true });
}
}
async function confirmDelete() {
const form = deleting;
setDeleting(null);
try {
await deleteEducationForm(form.id);
await reload();
} catch (error) {
setAlert({ text: error.message || 'Не удалось удалить форму обучения', isError: true });
}
}
return (
<>
<div className="card create-card">
<h2>Новая форма обучения</h2>
<form onSubmit={submit}>
<div className="form-row">
<div className="form-group">
<label htmlFor="new-ef-name">Название</label>
<input
type="text"
id="new-ef-name"
placeholder="Бакалавриат"
required
value={name}
onChange={event => setName(event.target.value)}
/>
</div>
<button type="submit" className="btn btn-md btn-primary">Создать</button>
</div>
<div
className={`form-alert${alert.text ? (alert.isError ? ' error' : ' success') : ''}`}
role="alert"
>
{alert.text}
</div>
</form>
</div>
<div className="card">
<h2>Все формы обучения</h2>
<div className="table-wrap">
<table>
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{loadError ? (
<tr><td colSpan="3" className="loading-row">Ошибка загрузки</td></tr>
) : forms === null ? (
<tr><td colSpan="3" className="loading-row">Загрузка...</td></tr>
) : forms.length === 0 ? (
<tr><td colSpan="3" className="loading-row">Нет форм обучения</td></tr>
) : forms.map(form => (
<tr key={form.id}>
<td>{form.id}</td>
<td>{form.name}</td>
<td>
<button
type="button"
className="btn btn-sm btn-danger"
onClick={() => setDeleting(form)}
>
Удалить
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{deleting && (
<ConfirmDialog
title="Удаление формы обучения"
message="Удалить форму обучения?"
confirmLabel="Удалить"
danger
onClose={() => setDeleting(null)}
onConfirm={() => void confirmDelete()}
/>
)}
</>
);
}