// Карточка КИО. Один и тот же компонент рисуется у курсанта, на пульте, // на внешнем мониторе и на АРМ ДДС — там в режиме `readOnly`. import { useSigns } from "@/shared/api/http"; import { type CardState, display } from "./merge"; import { type Field, visibleGroups } from "./fields"; interface Props { state: CardState; /** Обязательные поля приходят сценарием в `call.incoming`. */ required?: string[]; readOnly?: boolean; onChange?: (path: string, value: unknown) => void; } export function KioCard({ state, required = [], readOnly = false, onChange }: Props) { const dds = display(state, "dds") as string | null; return (
{visibleGroups(dds).map((group) => ( {group.fields.map((field) => ( ))}
{group.title}
))}
); } function Row({ field, value, pending, required, readOnly, onChange, }: { field: Field; value: unknown; pending: boolean; required: boolean; readOnly?: boolean; onChange?: (path: string, value: unknown) => void; }) { const empty = value === null || value === undefined || value === ""; return ( {field.label} {required && ·} {readOnly ? ( {formatValue(value)} ) : ( )} ); } function Input({ field, value, onChange, }: { field: Field; value: unknown; onChange?: (path: string, value: unknown) => void; }) { const emit = (next: unknown) => onChange?.(field.path, next); if (field.kind === "list") { return {formatValue(value)}; } if (field.kind === "bool") { return ( emit(e.target.checked)} /> ); } if (field.kind === "select") { return ( ); } if (field.kind === "signs") { return emit(next)} />; } if (field.kind === "number") { return ( { const text = e.target.value; if (text === "") return emit(null); const parsed = Number(text); // Нечисловое значение не уходит на сервер: карточка — норматив, // а не свободный текст. if (Number.isFinite(parsed) && parsed >= 0) emit(parsed); }} /> ); } return ( emit(e.target.value)} /> ); } /** Три признака происшествия каскадом: выбор верхнего сужает нижний. * * Значения приходят с сервера, а не лежат на фронте: классификатор — 1283 строки, * и он обновляется файлом от заказчика (docs/spec/DATASET.md). Служба из выбора * не следует — её считает сервер, поэтому здесь её нет. */ function SignsPicker({ value, onChange, }: { value: string[]; onChange: (next: string[]) => void; }) { const levels = [0, 1, 2]; return (
{levels.map((level) => ( { // Смена верхнего признака обнуляет нижние: они относились // к другой ветке классификатора. const next = value.slice(0, level); if (sign) next.push(sign); onChange(next); }} /> ))}
); } function SignSelect({ prefix, chosen, onPick, }: { prefix: string[]; chosen: string; onPick: (sign: string) => void; }) { const page = useSigns(prefix); const values = page.data?.values ?? []; // Уровня может не быть: часть происшествий классифицируется двумя признаками. if (!page.isLoading && values.length === 0) return null; return ( ); } function formatValue(value: unknown): string { if (value === true) return "да"; if (value === false) return "нет"; if (Array.isArray(value)) return value.length ? value.join(", ") : "—"; if (value === null || value === undefined || value === "") return "—"; return String(value); }