arm-web/src/widgets/DocumentReview/statementGrid.ts
Andrey Epifancev 9e89b9fde7 feat(review): шаблонные строки ББ/ОФР и выделение строк «БАЛАНС» (#19)
Фронт к RSHB/arm-core#80. Строки и ячейки с id = null (достроены сервером)
редактируются; правка уходит в PATCH по line_code, строки из БД — по line_id.
Ключ достроенной строки code-<код> (R-10). Строки 1600/1700 формы BALANCE
выделены фоном #f9f0ff и жирным (R-7). OpenSpec change statement-template-lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 12:18:44 +04:00

92 lines
3.8 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';
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,
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}`;
/** Черновик → тело 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 };
}