lct-hack/frontend/src/features/kio-card/fields.ts
Ivan Gerasimov 48c936ea6e lct-10: карточка КИО — форма, слияние правок, read-only
Фронт не хранит карточку как истину: локально живут только правки
курсанта, ещё не подтверждённые сервером. Распознанное сервером значение
перекрывает набранное, эхо своей правки её подтверждает. Иначе
автозаполнение и ручной ввод дерутся за одно поле — худший класс багов
для отладки в последнюю ночь.

Описание полей отдельно от отрисовки: по нему же карточка рисуется
read-only на мониторе, пульте и АРМ ДДС.

Сверка формы с контрактом встроена в make typecheck: все 40 полей
домена на форме, а поле, добавленное в контракт и забытое в форме,
теперь поймается само. Это пропущенный факт по вине интерфейса,
а не курсанта.

Открытый вопрос в карточке: нужно ли автозаполнение карточки из речи
вообще — полнота КИО оценивается кодом E5, и если сервер впишет адрес
сам, курсант перестанет за неё отвечать.
2026-09-17 21:16:51 +03:00

147 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Поля карточки информационного обмена. Это не дизайн формы, это норматив:
// docs/spec/NORMATIVES.md#карточка-информационного-обмена-кио
//
// Описание полей отдельно от отрисовки: по нему же рисуется карточка на внешнем
// мониторе, на пульте и на АРМ ДДС — там она read-only.
import type { DDSCode, IncidentType } from "@/shared/types/generated";
export type FieldKind = "text" | "number" | "bool" | "select";
export interface Field {
/** Путь в карточке: «floor», «fire.floors» — так же, как в `kio.patch`. */
path: string;
label: string;
kind: FieldKind;
options?: { value: string; label: string }[];
/** Заполняется системой: номер определяется автоматически, время — сервером. */
readOnly?: boolean;
}
export interface FieldGroup {
title: string;
fields: Field[];
/** Группа показывается только для этой службы. */
dds?: DDSCode;
}
const INCIDENT: { value: IncidentType; label: string }[] = [
{ value: "fire", label: "Пожар" },
{ value: "medical", label: "Медицинский вызов" },
{ value: "police", label: "Правонарушение" },
{ value: "gkh", label: "Авария ЖКХ" },
{ value: "era_glonass", label: "ДТП, автовызов" },
{ value: "other", label: "Прочее" },
];
const DDS: { value: DDSCode; label: string }[] = [
{ value: "01", label: "01 — МЧС" },
{ value: "02", label: "02 — полиция" },
{ value: "03", label: "03 — скорая помощь" },
{ value: "04", label: "04 — прочие службы" },
{ value: "gkh", label: "ЖКХ" },
];
export const GROUPS: FieldGroup[] = [
{
title: "Служебное",
fields: [
{ path: "registered_at", label: "Время регистрации", kind: "text", readOnly: true },
{ path: "response_status", label: "Статус реагирования", kind: "text", readOnly: true },
],
},
{
title: "Звонящий",
fields: [
{ path: "caller_number", label: "Номер телефона", kind: "text", readOnly: true },
{ path: "caller_name", label: "ФИО", kind: "text" },
{ path: "caller_contact", label: "Контактный телефон", kind: "text" },
{ path: "language", label: "Язык общения", kind: "text" },
],
},
{
title: "Место",
fields: [
{ path: "okato", label: "ОКАТО", kind: "text" },
{ path: "address", label: "Адрес", kind: "text" },
{ path: "street", label: "Улица", kind: "text" },
{ path: "building", label: "Дом", kind: "text" },
{ path: "entrance", label: "Подъезд", kind: "text" },
{ path: "floor", label: "Этаж", kind: "text" },
{ path: "intercom_code", label: "Код домофона", kind: "text" },
],
},
{
title: "Происшествие",
fields: [
{ path: "incident_type", label: "Тип происшествия", kind: "select", options: INCIDENT },
{ path: "description", label: "Описание", kind: "text" },
{ path: "victims_count", label: "Пострадавших", kind: "number" },
{ path: "is_emergency", label: "Признак ЧС", kind: "bool" },
{ path: "life_threat", label: "Угроза жизни", kind: "bool" },
{ path: "evacuation_needed", label: "Нужна эвакуация", kind: "bool" },
],
},
{
title: "Дежурно-диспетчерская служба",
fields: [
{ path: "dds", label: "Служба", kind: "select", options: DDS },
{ path: "dispatch_order_at", label: "Приказ на выезд", kind: "text", readOnly: true },
{ path: "arrival_at", label: "Прибытие", kind: "text", readOnly: true },
],
},
{
title: "Уточнения: пожар",
dds: "01",
fields: [
{ path: "fire.fire_nature", label: "Характер пожара", kind: "text" },
{ path: "fire.object_kind", label: "Объект", kind: "text" },
{ path: "fire.floors", label: "Этажность", kind: "number" },
{ path: "fire.gasified", label: "Газификация", kind: "bool" },
{ path: "fire.people_inside", label: "Люди в помещении", kind: "bool" },
{ path: "fire.smoke_spread", label: "Куда идёт дым", kind: "text" },
],
},
{
title: "Уточнения: полиция",
dds: "02",
fields: [
{ path: "police.offence_kind", label: "Вид правонарушения", kind: "text" },
{ path: "police.suspects", label: "Подозреваемые, приметы", kind: "text" },
{ path: "police.suspect_fled", label: "Нарушитель скрылся", kind: "bool" },
{ path: "police.vehicle", label: "Транспортное средство", kind: "text" },
],
},
{
title: "Уточнения: медицина",
dds: "03",
fields: [
{ path: "medical.reason", label: "Повод к вызову", kind: "text" },
{ path: "medical.conscious", label: "В сознании", kind: "bool" },
{ path: "medical.breathing", label: "Дышит", kind: "bool" },
{ path: "medical.can_move", label: "Может передвигаться", kind: "bool" },
{ path: "medical.age", label: "Возраст", kind: "number" },
],
},
{
title: "Уточнения: ЖКХ и прочие",
dds: "gkh",
fields: [
{ path: "utility.failure_kind", label: "Тип аварии", kind: "text" },
{ path: "utility.scale", label: "Масштаб", kind: "text" },
{ path: "utility.threat_to_residents", label: "Угроза жильцам", kind: "bool" },
],
},
];
/** Группы, которые показываются при выбранной службе. */
export function visibleGroups(dds: string | null | undefined): FieldGroup[] {
return GROUPS.filter((group) => !group.dds || group.dds === dds);
}
export function readValue(card: Record<string, unknown>, path: string): unknown {
return path.split(".").reduce<unknown>(
(value, part) => (value && typeof value === "object" ? (value as Record<string, unknown>)[part] : undefined),
card,
);
}