Implement local task 09 training workflows
This commit is contained in:
parent
e081fa410b
commit
cec84ffcd0
45 changed files with 2679 additions and 366 deletions
|
|
@ -8,13 +8,14 @@ import { useEffect, useRef, useState } from "react";
|
|||
|
||||
import { Debrief } from "@/features/debrief/Debrief";
|
||||
import { Director } from "@/features/instructor/Director";
|
||||
import { ScenarioEditor } from "@/features/instructor/ScenarioEditor";
|
||||
import { useControl } from "@/features/instructor/useControl";
|
||||
import { KioCard } from "@/features/kio-card/KioCard";
|
||||
import { StandStatus } from "@/features/health/StandStatus";
|
||||
import { ModeBanner } from "@/features/mode-banner/ModeBanner";
|
||||
import { useObserve } from "@/features/observe/useObserve";
|
||||
import { InterviewTimer, Timers } from "@/features/timers/Timers";
|
||||
import { useScenarios, useSessions } from "@/shared/api/http";
|
||||
import { useScenarios, useSessions, useTrainees } from "@/shared/api/http";
|
||||
import { sessionIdFromUrl } from "@/shared/api/session";
|
||||
import type { SessionMode } from "@/shared/types/generated";
|
||||
|
||||
|
|
@ -28,7 +29,9 @@ export function Instructor() {
|
|||
const [sessionId, setSessionId] = useState<string | null>(sessionIdFromUrl());
|
||||
const [scenarioId, setScenarioId] = useState("");
|
||||
const [mode, setMode] = useState<SessionMode>("training");
|
||||
const [exercise, setExercise] = useState<"dds" | "call" | "card">("dds");
|
||||
const [trainee, setTrainee] = useState("");
|
||||
const [traineeId, setTraineeId] = useState("");
|
||||
const [noteFor, setNoteFor] = useState<string | null>(null);
|
||||
// Запускается только занятие, созданное этой кнопкой. Открытая из истории
|
||||
// ссылка на идущее занятие не должна запускать его заново и сбрасывать.
|
||||
|
|
@ -37,6 +40,7 @@ export function Instructor() {
|
|||
|
||||
const scenarios = useScenarios();
|
||||
const history = useSessions();
|
||||
const trainees = useTrainees();
|
||||
const session = useObserve(sessionId);
|
||||
const control = useControl(sessionId);
|
||||
|
||||
|
|
@ -45,6 +49,7 @@ export function Instructor() {
|
|||
}, [scenarios.data, scenarioId]);
|
||||
|
||||
function startLesson() {
|
||||
if (trainees.data?.length && !traineeId) return;
|
||||
const id = crypto.randomUUID();
|
||||
// Номер занятия живёт в адресе: монитор и АРМ курсанта открываются ссылкой.
|
||||
window.history.replaceState(null, "", `/instructor?session=${id}`);
|
||||
|
|
@ -56,8 +61,9 @@ export function Instructor() {
|
|||
useEffect(() => {
|
||||
if (!sessionId || control.status !== "open" || toStart.current !== sessionId) return;
|
||||
toStart.current = null;
|
||||
control.start(scenarioId, mode, trainee || "Курсант");
|
||||
}, [sessionId, control, scenarioId, mode, trainee]);
|
||||
const selected = trainees.data?.find((item) => item.id === traineeId);
|
||||
control.start(scenarioId, mode, selected?.name || trainee || "Демо-курсант", exercise, undefined, selected?.id);
|
||||
}, [sessionId, control, scenarioId, mode, trainee, traineeId, trainees.data, exercise]);
|
||||
|
||||
const links = sessionId
|
||||
? {
|
||||
|
|
@ -90,6 +96,17 @@ export function Instructor() {
|
|||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Упражнение</th>
|
||||
<td>
|
||||
<label className="mode-choice"><input type="radio" checked={exercise === "dds"}
|
||||
onChange={() => setExercise("dds")} /> Диспетчер ДДС · готовая карточка</label>
|
||||
<label className="mode-choice"><input type="radio" checked={exercise === "call"}
|
||||
onChange={() => setExercise("call")} /> Оператор 112 · звонок</label>
|
||||
<label className="mode-choice"><input type="radio" checked={exercise === "card"}
|
||||
onChange={() => setExercise("card")} /> Оператор 112 · текстовая вводная</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Режим</th>
|
||||
<td>
|
||||
|
|
@ -108,14 +125,27 @@ export function Instructor() {
|
|||
<tr>
|
||||
<th>Курсант</th>
|
||||
<td>
|
||||
<input type="text" value={trainee} placeholder="фамилия"
|
||||
onChange={(event) => setTrainee(event.target.value)} />
|
||||
{trainees.data?.length ? (
|
||||
<select value={traineeId} onChange={(event) => setTraineeId(event.target.value)}>
|
||||
<option value="">Выберите учётную запись</option>
|
||||
{trainees.data.map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.name}{item.group ? ` · ${item.group}` : ""}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<>
|
||||
<input type="text" value={trainee} placeholder="имя для демо"
|
||||
onChange={(event) => setTrainee(event.target.value)} />
|
||||
<small className="ref">Без учётной записи: личный отчёт курсанту недоступен.</small>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<button type="button" className="start" onClick={startLesson} disabled={!scenarioId}>
|
||||
<button type="button" className="start" onClick={startLesson}
|
||||
disabled={!scenarioId || trainees.isLoading || Boolean(trainees.data?.length && !traineeId)}>
|
||||
Начать занятие
|
||||
</button>
|
||||
</p>
|
||||
|
|
@ -127,12 +157,12 @@ export function Instructor() {
|
|||
</p>
|
||||
)}
|
||||
{sessionId && !session.ended && (
|
||||
<Director onInject={control.inject} disabled={control.status !== "open"} error={session.error} />
|
||||
exercise === "call" && <Director onInject={control.inject} disabled={control.status !== "open"} error={session.error} />
|
||||
)}
|
||||
{links && (
|
||||
<table className="grid">
|
||||
<tbody>
|
||||
<tr><th>АРМ курсанта</th><td><a href={links.trainee}>открыть</a></td></tr>
|
||||
<tr><th>АРМ курсанта</th><td><a href={exercise === "dds" ? links.dds : links.trainee}>открыть {exercise === "dds" ? "ДДС" : "112"}</a></td></tr>
|
||||
<tr><th>Внешний монитор</th><td><a href={links.wall}>открыть</a></td></tr>
|
||||
<tr><th>АРМ ДДС</th><td><a href={links.dds}>открыть</a></td></tr>
|
||||
</tbody>
|
||||
|
|
@ -204,6 +234,11 @@ export function Instructor() {
|
|||
</section>
|
||||
</div>
|
||||
|
||||
<ScenarioEditor scenarios={scenarios.data ?? []} onApproved={async (id) => {
|
||||
setScenarioId(id);
|
||||
await scenarios.refetch();
|
||||
}} />
|
||||
|
||||
{session.report && <Debrief report={session.report} />}
|
||||
</main>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue