Andrey Epifancev 4ba339914c fix(web): синхронизация встроенного фронта с arm-web@test (1237105) (#120)
Перенесён диапазон arm-web 025233f..1237105 (3 коммита, 8 файлов набора синхронизации)
по шагам docs/scripts/sync-web.sh. После переноса web/ и arm-web@test идентичны по набору.
Коммиты arm-web:
- 54f88e9 feat(DocumentReview): подстроки под родителем, пустые строки детализации свёрнуты (#34, AURORA-1527)
- 85e1b86 fix(ChecksPanel): заголовок мягких проверок «Предупреждения проверок (N)» (#34, AURORA-1527)
- a4f5481 fix(DocumentReview): label переключателя пустых строк связан через htmlFor (#36, AURORA-1527)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:41:01 +04:00

109 lines
5.0 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.

import type { CellPatch, ColumnOut, StatementOut, ValueOut } from '../../types/api';
export type GridRow = {
key: string;
lineId: string | null;
code: string | null;
name: string | null;
origin: 'source' | 'template';
/** Родитель строки детализации — отдаёт ядро (`parent_code`); своего правила на фронте нет. */
parentCode: string | null;
cells: Record<string, ValueOut | undefined>;
};
/** Ключ строки: id из БД, у достроенной строки (id = null) — код; двоеточия в ключе нет (разделитель cellKey). */
const rowKey = (id: string | null, code: string | null) => id ?? `code-${code ?? ''}`;
export function buildStatementGrid(statement: StatementOut): { columns: ColumnOut[]; rows: GridRow[] } {
const columns = [...statement.columns].sort((a, b) => a.column_index - b.column_index);
const rows = statement.lines.map((line) => {
const cells: Record<string, ValueOut | undefined> = {};
for (const value of line.values) {
cells[value.column_id] = value;
}
return {
key: rowKey(line.id, line.line_code),
lineId: line.id,
code: line.line_code,
name: line.line_name,
origin: line.origin,
parentCode: line.parent_code ?? null,
cells,
};
});
return { columns, rows };
}
/** Уверенность — строка Decimal «0..100»; сравнение с 100 — это не сумма, float здесь безопасен. */
export function cellFlags(value: ValueOut | undefined): { attention: boolean; modified: boolean } {
if (!value) {
return { attention: false, modified: false };
}
const lowConfidence = value.confidence !== null && Number(value.confidence) < 100;
return { attention: lowConfidence || value.is_recognized === false, modified: value.is_modified };
}
export const cellKey = (rowKeyValue: string, columnId: string) => `${rowKeyValue}:${columnId}`;
/** Ячейка со значением: сумма (в т. ч. нулевая) или текст; пусто — только null/пустая строка. */
const hasValue = (value: ValueOut | undefined) => Boolean(value) && ((value?.amount ?? '') !== '' || (value?.string_value ?? '') !== '');
/**
* Ключи пустых строк детализации (parent_code != null): ни в одной колонке нет значения и нет правки в черновике.
* Ноль — значение, такая строка не пуста. Только показ: данные и PATCH не затрагиваются.
*/
export function emptySublineKeys(rows: GridRow[], draft: Record<string, string>): Set<string> {
const edited = new Set(Object.keys(draft).map((key) => key.slice(0, key.lastIndexOf(':'))));
return new Set(
rows.filter((row) => row.parentCode !== null && !edited.has(row.key) && !Object.values(row.cells).some(hasValue)).map((row) => row.key),
);
}
/** Черновик → тело PATCH: строка из БД адресуется line_id, достроенная — line_code (сервер создаст её). */
export function draftToPatch(draft: Record<string, string | null>, rows: GridRow[]): CellPatch[] {
const byKey = new Map(rows.map((row) => [row.key, row]));
return Object.entries(draft).map(([key, amount]) => {
const sep = key.lastIndexOf(':');
const row = byKey.get(key.slice(0, sep));
const column_id = key.slice(sep + 1);
if (!row) {
throw new Error(`строка черновика ${key} не найдена в форме`);
}
return row.lineId ? { line_id: row.lineId, column_id, amount } : { line_code: row.code ?? '', column_id, amount };
});
}
/** Каноничный вид суммы для сравнения строк: без хвостовых нулей дроби и лишних ведущих нулей. */
function normalizeAmount(value: string | null): string | null {
if (value === null) {
return null;
}
const match = /^(-?)(\d+)(?:\.(\d*))?$/.exec(value);
if (!match) {
return value;
}
const int = match[2].replace(/^0+(?=\d)/, '');
const frac = (match[3] ?? '').replace(/0+$/, '');
const body = frac ? `${int}.${frac}` : int;
return body === '0' ? '0' : `${match[1]}${body}`;
}
/**
* Обновить черновик после ввода в ячейку: если значение совпало с исходным — ключ убирается
* (в PATCH уходят только реальные изменения). Неверный формат остаётся в черновике (подсвечивается и блокирует сохранение).
*/
export function applyDraftInput(
draft: Record<string, string>,
key: string,
text: string,
originalAmount: string | null,
parse: (input: string) => { ok: true; value: string | null } | { ok: false },
): Record<string, string> {
const parsed = parse(text);
if (parsed.ok && normalizeAmount(parsed.value) === normalizeAmount(originalAmount)) {
const { [key]: _removed, ...rest } = draft;
return rest;
}
return { ...draft, [key]: text };
}