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:
80
frontend/react/teacher/RequestsTab.jsx
Normal file
80
frontend/react/teacher/RequestsTab.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
changeRequestTarget,
|
||||
changeTypeLabel,
|
||||
formatInstant,
|
||||
preferenceStatusLabel
|
||||
} from './teacher-service.js';
|
||||
import { formatIsoDate, trimTime } from '../shared/dates.js';
|
||||
|
||||
export function RequestsTab({ items, onCancel, onRefresh, stateKind }) {
|
||||
return (
|
||||
<>
|
||||
<section className="request-ledger-head">
|
||||
<div>
|
||||
<span>Журнал решений</span>
|
||||
<h2>Заявки на изменение занятий</h2>
|
||||
<p>Откройте занятие в расписании, чтобы запросить перенос, другую аудиторию или отмену. Здесь сохраняются статус, комментарий и вся история решения.</p>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary" type="button" onClick={onRefresh}>Обновить</button>
|
||||
</section>
|
||||
<div className="request-ledger" aria-live="polite">
|
||||
{stateKind === 'error' ? (
|
||||
<div className="request-empty error">Не удалось загрузить заявки</div>
|
||||
) : stateKind === 'loading' ? (
|
||||
<div className="request-empty">Загрузка заявок...</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="request-empty">Заявок на изменение пока нет. Откройте занятие во вкладке «Расписание».</div>
|
||||
) : items.map(item => (
|
||||
<RequestLedgerItem key={item.id} item={item} onCancel={onCancel} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RequestLedgerItem({ item, onCancel }) {
|
||||
const history = item.history || [];
|
||||
const statusClass = String(item.status || '').toLowerCase();
|
||||
|
||||
return (
|
||||
<article className="request-ledger-item">
|
||||
<div className={`request-ledger-stripe ${statusClass}`} />
|
||||
<div className="request-ledger-main">
|
||||
<span>{formatIsoDate(item.lessonDate)} · {trimTime(item.sourceStartTime)}–{trimTime(item.sourceEndTime)}</span>
|
||||
<h3>{item.subjectName}</h3>
|
||||
<p>{(item.groupNames || []).join(', ')} · {item.sourceClassroomName}</p>
|
||||
<div className="request-change-line">
|
||||
<b>{changeTypeLabel(item.requestType)}</b>
|
||||
<i>→</i>
|
||||
<strong>{changeRequestTarget(item)}</strong>
|
||||
</div>
|
||||
<p className="request-reason">{item.reason}</p>
|
||||
{item.reviewComment && <p className="request-review-comment">Решение: {item.reviewComment}</p>}
|
||||
<details>
|
||||
<summary>История решения · {history.length}</summary>
|
||||
<ol>
|
||||
{history.map((entry, index) => (
|
||||
<li key={index}>
|
||||
<time>{formatInstant(entry.createdAt)}</time>
|
||||
<strong>{preferenceStatusLabel(entry.status)}</strong>
|
||||
<span>{entry.comment || 'Без комментария'}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</details>
|
||||
</div>
|
||||
<div className="request-ledger-side">
|
||||
<span className={`request-status ${statusClass}`}>{preferenceStatusLabel(item.status)}</span>
|
||||
{item.status === 'PENDING' && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-danger-subtle"
|
||||
onClick={() => void onCancel(item.id)}
|
||||
>
|
||||
Отозвать
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user