Complete training workflow and acceptance hardening

This commit is contained in:
andreysk0304 2026-09-26 18:12:27 +03:00 • committed by gglamer
commit 7237265833
243 changed files with 17014 additions and 1500 deletions

View file

@ -20,7 +20,9 @@ export function KioCard({ state, required = [], readOnly = false, referenceArm =
const groups = visibleGroups(dds);
const section = (group: FieldGroup) => (
<section className={`kio-arm-section kio-arm-${group.area}`} key={group.title}>
{(!referenceArm || group.area !== "header") && <div className="kio-arm-section-title">{group.title}</div>}
{(!referenceArm || group.area !== "header") && <div className="kio-arm-section-title">
{referenceArm && group.area === "incident" ? "Введите тип происшествия" : group.title}
</div>}
<div className="kio-arm-fields">
{(referenceArm && group.area === "incident"
? [...group.fields].sort((a, b) => Number(b.path === "incident_type") - Number(a.path === "incident_type"))
@ -198,6 +200,7 @@ function SignsPicker({
{levels.map((level) => (
<SignSelect
key={level}
level={level}
prefix={value.slice(0, level)}
group={group}
chosen={value[level] ?? ""}
@ -218,8 +221,9 @@ function SignsPicker({
}
function SignSelect({
prefix, group, chosen, onPick,
level, prefix, group, chosen, onPick,
}: {
level: number;
prefix: string[];
group: number | null;
chosen: string;
@ -230,7 +234,7 @@ function SignSelect({
// Уровня может не быть: часть происшествий классифицируется двумя признаками.
if (!page.isLoading && values.length === 0) return null;
return (
<select aria-label={`Признак происшествия ${prefix.length + 1}`} value={chosen} onChange={(e) => onPick(e.target.value)} disabled={page.isLoading}>
<select aria-label={`Признак происшествия ${level + 1}`} value={chosen} onChange={(e) => onPick(e.target.value)} disabled={page.isLoading}>
<option value="">—</option>
{values.map((sign) => (
<option key={sign} value={sign}>{sign}</option>

View file

@ -15,6 +15,24 @@ export interface CardState {
export const empty: CardState = { server: {}, pending: {} };
function sameValue(left: unknown, right: unknown): boolean {
if (Object.is(left, right)) return true;
if (Array.isArray(left) || Array.isArray(right)) {
return Array.isArray(left) && Array.isArray(right)
&& left.length === right.length
&& left.every((item, index) => sameValue(item, right[index]));
}
if (left && right && typeof left === "object" && typeof right === "object") {
const leftRecord = left as Record<string, unknown>;
const rightRecord = right as Record<string, unknown>;
const keys = Object.keys(leftRecord);
return keys.length === Object.keys(rightRecord).length
&& keys.every((key) => key in rightRecord
&& sameValue(leftRecord[key], rightRecord[key]));
}
return false;
}
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 };
@ -42,7 +60,7 @@ export function applyPatch(
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];
if (source === "auto" || sameValue(pending[path], value)) delete pending[path];
}
return { server, pending };
}
@ -51,7 +69,7 @@ export function applyPatch(
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];
if (sameValue(readPath(card, path), pending[path])) delete pending[path];
}
return { server: card, pending };
}