Implement DDS exercise, customer UI and local demo

This commit is contained in:
andreysk0304 2026-09-21 19:56:13 +03:00
commit 0526b11f91
46 changed files with 2504 additions and 253 deletions

View file

@ -1,7 +1,7 @@
// HTTP-клиент и хуки. Типы ответов HTTP в generated.ts пока не попадают —
// там только события WS; здесь описаны руками по docs/arch/CONTRACT.md#http-api.
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { Level, SessionMode } from "@/shared/types/generated";
@ -32,6 +32,7 @@ export interface Health {
embeddings_ready: boolean;
scenarios_loaded: number;
offline: boolean;
demo_no_db?: boolean;
}
export interface ScenarioSummary {
@ -125,6 +126,60 @@ export function useSigns(prefix: string[]) {
export const useTrainees = () =>
useQuery({ queryKey: ["trainees"], queryFn: () => request<TraineeInfo[]>("/api/trainees") });
export interface GroupInfo {
id: string;
name: string;
}
export interface GroupAnalytics {
group: GroupInfo;
enrolled_trainees: number;
active_trainees: number;
scored_attempts: number;
average_score: number | null;
errors: {
code: string;
title: string;
affected_trainees: number;
occurrences: number;
rate_percent: number;
recommendation: string;
}[];
}
export const useGroups = () =>
useQuery({ queryKey: ["groups"], queryFn: () => request<GroupInfo[]>("/api/groups") });
export function useCreateGroup() {
const cache = useQueryClient();
return useMutation({
mutationFn: (name: string) => request<GroupInfo>("/api/groups", {
method: "POST", body: JSON.stringify({ name }),
}),
onSuccess: () => void cache.invalidateQueries({ queryKey: ["groups"] }),
});
}
export function useAssignTrainee() {
const cache = useQueryClient();
return useMutation({
mutationFn: ({ groupId, traineeId }: { groupId: string; traineeId: string }) =>
request<GroupInfo>(`/api/groups/${groupId}/trainees/${traineeId}`, { method: "PUT" }),
onSuccess: () => {
void cache.invalidateQueries({ queryKey: ["trainees"] });
void cache.invalidateQueries({ queryKey: ["group-analytics"] });
},
});
}
export const useGroupAnalytics = (groupId: string | null) =>
useQuery({
queryKey: ["group-analytics", groupId],
queryFn: () => request<GroupAnalytics>(`/api/groups/${groupId}/analytics`),
enabled: Boolean(groupId),
refetchInterval: 15_000,
});
export const useProfile = (traineeId: string | null) =>
useQuery({
queryKey: ["profile", traineeId],

View file

@ -87,12 +87,25 @@ export interface CardBriefing {
card?: KIO | null;
}
export interface CardNext {
type: "card.next";
card_id: string;
}
/** Снимок КИО. После передачи не меняется — оператор не дописывает задним числом. */
export interface CardReceived {
type: "card.received";
card: KIO;
from_operator: string;
at: string;
card_index?: number;
card_total?: number;
}
export interface CardReply {
type: "card.reply";
card_id: string;
text: string;
}
/** Статус карточки целиком — считается из статусов служб, руками не ставится. */
@ -134,6 +147,23 @@ export interface CrewSelect {
/** Дежурно-диспетчерская служба, в которую уходит карточка. */
export type DDSCode = "01" | "02" | "03" | "04" | "gkh";
export interface DdsCardReport {
card_id: string;
scenario_id: string;
score_auto: number;
reply_text: string;
metrics: Array<Metric>;
findings: Array<Finding>;
actions?: Array<Record<string, unknown>>;
duration_ms?: number;
}
export interface DdsCardSummary {
card_id: string;
scenario_id: string;
score_auto: number;
}
/** Передача в ДДС. При маршрутизации по ЕКП служба уже есть в notify. */
export interface DdsDispatch {
type: "dds.dispatch";
@ -387,6 +417,7 @@ export interface ScenarioPublish {
export interface ScenarioStart {
type: "scenario.start";
scenario_id: string;
scenario_ids?: Array<string> | null;
trainee: string;
trainee_id?: string | null;
group_id?: string | null;
@ -455,6 +486,7 @@ export interface SessionReport {
transcript: Array<TranscriptEntry>;
findings: Array<Finding>;
metrics: Array<Metric>;
card_results?: Array<DdsCardReport>;
competencies: Array<CompetencyScore>;
reference_questions: Array<HintShown>;
missed_checklist: Array<string>;
@ -507,6 +539,11 @@ export interface StationSnapshot {
crew_options?: Array<string>;
crew_selected?: string | null;
phone_reports?: Array<PhoneReportRecord>;
card_id?: string | null;
card_index?: number;
card_total?: number;
reply_text?: string;
completed_cards?: Array<DdsCardSummary>;
}
/** Состояние АРМ ДДС после каждой отметки: что стоит и что доступно дальше. */
@ -614,7 +651,7 @@ export type InstructorToServer = ScenarioStart | DirectorInject | ReferencePlay
export type ServerToStation = CardReceived | StationState | PhoneReport | TimerTick | SessionEnded | ScoreReady | ErrorEvent;
export type StationToServer = CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | StationFinish | ZoneDecision | CrewDispatched | CrewArrived;
export type StationToServer = CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | StationFinish | CardReply | CardNext | ZoneDecision | CrewDispatched | CrewArrived;
export type EventName =
"bg.start"
@ -630,7 +667,9 @@ export type EventName =
| "card.ack"
| "card.bounce"
| "card.briefing"
| "card.next"
| "card.received"
| "card.reply"
| "card.status"
| "card.submit"
| "crew.arrived"