Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
81 lines
3.9 KiB
JavaScript
81 lines
3.9 KiB
JavaScript
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>
|
||
);
|
||
}
|