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; }; /** Ключ строки: 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 = {}; 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): Set { 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, 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, key: string, text: string, originalAmount: string | null, parse: (input: string) => { ok: true; value: string | null } | { ok: false }, ): Record { const parsed = parse(text); if (parsed.ok && normalizeAmount(parsed.value) === normalizeAmount(originalAmount)) { const { [key]: _removed, ...rest } = draft; return rest; } return { ...draft, [key]: text }; }