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

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

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

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

69 lines
3.3 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.

// Кто выигрывает спор за поле карточки.
//
// Истина — на сервере: он распознаёт факты из речи и присылает `kio.patch`
// с `source: "auto"`. Локально хранится только то, что курсант печатает
// прямо сейчас, — до подтверждения сервером. Иначе автозаполнение и ручной
// ввод дерутся за одно поле, и это худший класс багов для отладки в последнюю
// ночь (docs/arch/FRONTEND.md).
export interface CardState {
/** Состояние с сервера — единственная истина. */
server: Record<string, unknown>;
/** Правки курсанта, ещё не подтверждённые сервером: путь → значение. */
pending: Record<string, unknown>;
}
export const empty: CardState = { server: {}, pending: {} };
export function setNested(card: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> {
const [head, ...rest] = path.split(".");
if (rest.length === 0) return { ...card, [head]: value };
const nested = (card[head] as Record<string, unknown>) ?? {};
return { ...card, [head]: setNested(nested, rest.join("."), value) };
}
/** Курсант ввёл значение: показываем сразу, отправку делает вызывающий. */
export function edit(state: CardState, path: string, value: unknown): CardState {
return { ...state, pending: { ...state.pending, [path]: value } };
}
/**
* Пришёл `kio.patch` от сервера.
*
* `auto` — сервер распознал факт из речи: он побеждает, локальная правка снимается.
* `operator` — эхо правки: снимаем ожидание, если сервер подтвердил то же значение.
*/
export function applyPatch(
state: CardState,
fields: Record<string, unknown>,
source: "auto" | "operator",
): CardState {
let server = state.server;
const pending = { ...state.pending };
for (const [path, value] of Object.entries(fields)) {
server = setNested(server, path, value);
if (source === "auto" || pending[path] === value) delete pending[path];
}
return { server, pending };
}
/** Пришло состояние карточки целиком (`kio.state`, `session.snapshot`). */
export function applyState(state: CardState, card: Record<string, unknown>): CardState {
const pending = { ...state.pending };
for (const path of Object.keys(pending)) {
if (readPath(card, path) === pending[path]) delete pending[path];
}
return { server: card, pending };
}
export function readPath(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,
);
}
/** Что показать в поле: своя неподтверждённая правка либо значение сервера. */
export function display(state: CardState, path: string): unknown {
return path in state.pending ? state.pending[path] : readPath(state.server, path);
}