Фронт не хранит карточку как истину: локально живут только правки курсанта, ещё не подтверждённые сервером. Распознанное сервером значение перекрывает набранное, эхо своей правки её подтверждает. Иначе автозаполнение и ручной ввод дерутся за одно поле — худший класс багов для отладки в последнюю ночь. Описание полей отдельно от отрисовки: по нему же карточка рисуется read-only на мониторе, пульте и АРМ ДДС. Сверка формы с контрактом встроена в make typecheck: все 40 полей домена на форме, а поле, добавленное в контракт и забытое в форме, теперь поймается само. Это пропущенный факт по вине интерфейса, а не курсанта. Открытый вопрос в карточке: нужно ли автозаполнение карточки из речи вообще — полнота КИО оценивается кодом E5, и если сервер впишет адрес сам, курсант перестанет за неё отвечать.
69 lines
3.3 KiB
TypeScript
69 lines
3.3 KiB
TypeScript
// Кто выигрывает спор за поле карточки.
|
||
//
|
||
// Истина — на сервере: он распознаёт факты из речи и присылает `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);
|
||
}
|