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:
149
frontend/react/settings/EduFormsTab.jsx
Normal file
149
frontend/react/settings/EduFormsTab.jsx
Normal 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()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user