// Профиль курсанта: радар компетенций, история попыток, дельта между ними. // // «Контроль подготовленности» из ТЗ — не таблица оценок, а инструмент // планирования подготовки. Работа над ошибками превращается в измеримый цикл: // посмотрел разбор → попробовал снова → увидел дельту (docs/product/MODES.md). import { useEffect, useState } from "react"; import { COMPETENCY_LABELS, Radar } from "@/features/debrief/Radar"; import { useMe } from "@/features/auth/useAuth"; import { clock } from "@/features/timers/clock"; import { useProfile, useTrainees } from "@/shared/api/http"; const MODE_LABELS: Record = { training: "тренировочный", exam: "контрольный", self: "самостоятельный", }; function sign(value: number | null, format: (value: number) => string): string { if (value === null) return "—"; if (value === 0) return "без изменений"; return `${value > 0 ? "+" : "−"}${format(Math.abs(value))}`; } export function Profile() { const url = new URLSearchParams(location.search); const [traineeId, setTraineeId] = useState(url.get("trainee")); const me = useMe(); const isTrainee = me.data?.role === "trainee"; const trainees = useTrainees(!isTrainee); const profile = useProfile(traineeId); useEffect(() => { if (isTrainee && me.data?.trainee_id) setTraineeId(me.data.trainee_id); }, [isTrainee, me.data?.trainee_id]); return (

Профиль курсанта

Открыть назначенные учебные материалы →

{!isTrainee &&

} {profile.data && (

{profile.data.trainee.name} {profile.data.trainee.group && ` · группа ${profile.data.trainee.group}`}

({ competency, value, }))} />

радар — среднее по попыткам; подсказок использовано всего: {profile.data.hints_total}

{profile.data.attempts.some((attempt) => attempt.score !== null) && (

Скачать сертификат PDF

)}

Дельта попыток

{profile.data.deltas.map((delta, index) => ( ))} {profile.data.deltas.length === 0 && ( )}
{delta.scenario_id} · {delta.from_attempt} → {delta.to_attempt} оценка {sign(delta.score, (value) => value.toFixed(0))} {" · "}опрос {sign(delta.interview_ms, (value) => clock(value))} {" · "}фактов {sign(delta.facts_got, (value) => String(value))}
повторных попыток по сценарию ещё не было

История занятий

{profile.data.attempts.map((attempt) => ( ))} {profile.data.attempts.length === 0 && }
{new Date(attempt.created_at).toLocaleString("ru-RU")}
{MODE_LABELS[attempt.mode] ?? attempt.mode} · попытка {attempt.attempt}
{attempt.scenario_id} · оценка {attempt.score?.toFixed(0) ?? "—"}
опрос {attempt.interview_ms === null ? "не завершён" : clock(attempt.interview_ms)} {attempt.facts_required !== null && ` · факты ${attempt.facts_got}/${attempt.facts_required}`} {attempt.hints ? ` · подсказок ${attempt.hints}` : ""} {Object.keys(attempt.codes).length > 0 && ` · ${Object.entries(attempt.codes).map(([code, count]) => `${code}×${count}`).join(", ")}`}
разбор {attempt.recording_available && <> {" · "} запись WAV }
занятий не было
)} {profile.isError &&

Профиль не загрузился.

} {!traineeId &&

Курсанты появляются после первого занятия.

}

Компетенции: {Object.values(COMPETENCY_LABELS).join(" · ")}

); }