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

@@ -9,207 +9,8 @@
<link rel="stylesheet" href="../ui-foundation.css">
</head>
<body>
<main class="shell">
<header class="topbar">
<div class="title">
<h1>Кабинет преподавателя</h1>
<span id="week-label">Загрузка периода...</span>
</div>
<div class="actions">
<button class="btn btn-icon-md btn-secondary" id="theme-toggle-local" type="button" aria-label="Переключить тему">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"></path>
</svg>
</button>
<a class="btn btn-danger-subtle" href="/" id="logout-link">Выйти</a>
</div>
</header>
<div id="teacher-root"></div>
<nav class="workspace-nav" aria-label="Разделы кабинета">
<button class="workspace-tab active" type="button" data-workspace-tab="schedule">Расписание</button>
<button class="workspace-tab" type="button" data-workspace-tab="preferences">Мои пожелания</button>
<button class="workspace-tab" type="button" data-workspace-tab="requests">
Заявки на изменение <span id="change-request-count">0</span>
</button>
</nav>
<section class="workspace-view active" data-workspace-view="schedule">
<section class="toolbar">
<div class="week-nav">
<button class="btn btn-icon-md btn-secondary" id="prev-week" type="button" aria-label="Предыдущая неделя">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<button class="btn btn-md btn-primary" id="today-week" type="button">Сегодня</button>
<button class="btn btn-icon-md btn-secondary" id="next-week" type="button" aria-label="Следующая неделя">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
<input class="date-input" id="date-picker" type="date" aria-label="Дата недели">
</section>
<section class="schedule-overview" id="schedule-overview" aria-live="polite"></section>
<div class="status" id="status-line"></div>
<section class="teacher-absence-panel">
<div class="teacher-absence-heading">
<div>
<span>Изменение планов</span>
<h2>Сообщить об отсутствии</h2>
<p>Заявка поступит кафедре на подтверждение. После согласования учебный отдел сможет подобрать замены.</p>
</div>
<button class="btn btn-md btn-secondary" id="absence-form-toggle" type="button" aria-expanded="false">
Новая заявка
</button>
</div>
<form class="teacher-absence-form" id="teacher-absence-form" hidden>
<label>
<span>Начало</span>
<input class="date-input" id="teacher-absence-start" type="date" required>
</label>
<label>
<span>Окончание</span>
<input class="date-input" id="teacher-absence-end" type="date" required>
</label>
<label class="teacher-absence-reason">
<span>Причина</span>
<input id="teacher-absence-reason" type="text" maxlength="2000" placeholder="Например, командировка" required>
</label>
<button class="btn btn-md btn-primary" type="submit">Отправить кафедре</button>
</form>
<div class="teacher-absence-status" id="teacher-absence-status"></div>
<div class="teacher-absence-list" id="teacher-absence-list" aria-live="polite"></div>
</section>
<section class="schedule" id="schedule-grid" aria-live="polite"></section>
</section>
<section class="workspace-view" data-workspace-view="preferences" hidden>
<section class="preference-hero">
<div>
<span class="preference-kicker">Контур доступности</span>
<h2>Соберите ритм семестра до составления расписания</h2>
<p>Строгая недоступность блокирует назначение. Мягкие отметки и пожелания к компактности помогают учебному отделу выбрать удобный вариант.</p>
</div>
<label class="preference-semester-field">
<span>Семестр</span>
<select id="preference-semester"></select>
</label>
</section>
<section class="preference-board">
<header class="preference-board-head">
<div>
<span>Недельный шаблон</span>
<h3>Отмечайте интервалы одним касанием</h3>
</div>
<label>
<span>Режим отметки</span>
<select id="preference-brush">
<option value="SOFT_PREFERRED">Предпочтительно</option>
<option value="SOFT_UNWANTED">Нежелательно</option>
<option value="HARD_UNAVAILABLE">Строго недоступен</option>
</select>
</label>
</header>
<div class="preference-legend" aria-label="Легенда">
<span class="is-preferred">Предпочтительно</span>
<span class="is-unwanted">Нежелательно</span>
<span class="is-hard">Строгий запрет</span>
<span class="is-pending">Ожидает согласования</span>
</div>
<div class="preference-grid-wrap">
<div class="preference-grid" id="preference-grid" aria-live="polite"></div>
</div>
<div class="preference-message" id="preference-message" role="status"></div>
</section>
<div class="preference-lower-grid">
<section class="preference-card">
<span class="preference-card-number">01</span>
<h3>Полная недоступность</h3>
<p>Добавьте отдельную дату внутри семестра — все пары этого дня будут считаться строгим ограничением.</p>
<form id="preference-date-form" class="preference-date-form">
<input type="date" id="preference-date" required>
<input type="text" id="preference-date-comment" maxlength="2000" placeholder="Комментарий (необязательно)">
<button class="btn btn-md btn-primary" type="submit">Добавить дату</button>
</form>
<div class="preference-date-list" id="preference-date-list"></div>
</section>
<section class="preference-card">
<span class="preference-card-number">02</span>
<h3>Компактность недели</h3>
<p>Эти мягкие пожелания не отменяют занятия автоматически, но видны при составлении правил.</p>
<label class="preference-toggle-card">
<input type="checkbox" id="preference-consecutive">
<span><strong>Пары подряд</strong><small>Собирать занятия последовательными блоками</small></span>
</label>
<label class="preference-toggle-card">
<input type="checkbox" id="preference-no-gaps">
<span><strong>Без окон</strong><small>Не оставлять пустые интервалы между занятиями</small></span>
</label>
</section>
</div>
</section>
<section class="workspace-view" data-workspace-view="requests" hidden>
<section class="request-ledger-head">
<div>
<span>Журнал решений</span>
<h2>Заявки на изменение занятий</h2>
<p>Откройте занятие в расписании, чтобы запросить перенос, другую аудиторию или отмену. Здесь сохраняются статус, комментарий и вся история решения.</p>
</div>
<button class="btn btn-md btn-secondary" id="change-request-refresh" type="button">Обновить</button>
</section>
<div class="request-ledger" id="change-request-list" aria-live="polite"></div>
</section>
</main>
<div class="request-dialog-overlay" id="request-dialog-overlay" hidden>
<section class="request-dialog" role="dialog" aria-modal="true" aria-labelledby="request-dialog-title">
<header>
<div>
<span>Предварительная проверка</span>
<h2 id="request-dialog-title">Заявка на изменение</h2>
<p id="request-dialog-lesson">Загрузка занятия...</p>
</div>
<button class="btn btn-icon-md btn-secondary" id="request-dialog-close" type="button" aria-label="Закрыть">×</button>
</header>
<form id="change-request-form">
<label>
<span>Что изменить</span>
<select id="change-request-type">
<option value="MOVE">Перенести дату или время</option>
<option value="CHANGE_CLASSROOM">Сменить аудиторию</option>
<option value="CANCEL">Отменить занятие</option>
</select>
</label>
<label data-request-field="date">
<span>Новая дата</span>
<select id="change-request-date"></select>
</label>
<label data-request-field="time">
<span>Новая пара</span>
<select id="change-request-time"></select>
</label>
<label data-request-field="classroom" hidden>
<span>Новая аудитория</span>
<select id="change-request-classroom"></select>
</label>
<label class="request-reason-field">
<span>Обоснование</span>
<textarea id="change-request-reason" maxlength="2000" rows="4" placeholder="Почему требуется изменение" required></textarea>
</label>
<div class="request-dialog-message" id="request-dialog-message" role="alert"></div>
<footer>
<button class="btn btn-md btn-secondary" id="request-dialog-cancel" type="button">Закрыть</button>
<button class="btn btn-md btn-primary" type="submit">Отправить в учебный отдел</button>
</footer>
</form>
</section>
</div>
<script type="module" src="app.js"></script>
<script type="module" src="/react/teacher.js"></script>
</body>
</html>