lct-10: карточка КИО — форма, слияние правок, read-only
Фронт не хранит карточку как истину: локально живут только правки курсанта, ещё не подтверждённые сервером. Распознанное сервером значение перекрывает набранное, эхо своей правки её подтверждает. Иначе автозаполнение и ручной ввод дерутся за одно поле — худший класс багов для отладки в последнюю ночь. Описание полей отдельно от отрисовки: по нему же карточка рисуется read-only на мониторе, пульте и АРМ ДДС. Сверка формы с контрактом встроена в make typecheck: все 40 полей домена на форме, а поле, добавленное в контракт и забытое в форме, теперь поймается само. Это пропущенный факт по вине интерфейса, а не курсанта. Открытый вопрос в карточке: нужно ли автозаполнение карточки из речи вообще — полнота КИО оценивается кодом E5, и если сервер впишет адрес сам, курсант перестанет за неё отвечать.
This commit is contained in:
parent
750ded2991
commit
48c936ea6e
8 changed files with 454 additions and 3 deletions
69
frontend/src/features/kio-card/merge.ts
Normal file
69
frontend/src/features/kio-card/merge.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Кто выигрывает спор за поле карточки.
|
||||
//
|
||||
// Истина — на сервере: он распознаёт факты из речи и присылает `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);
|
||||
}
|
||||
Loading…
Reference in a new issue