import type { ColumnOut, LineStatus, StatementOut, ValueOut } from '../../types/api'; import type { LineAction } from './statementDraft'; export type GridRow = { key: string; lineId: string | null; code: string | null; /** line_name — наименование из файла */ name: string | null; /** справочное имя строки по коду редакции */ referenceName: string | null; origin: 'source' | 'template'; /** Родитель строки детализации — отдаёт ядро (`parent_code`); своего правила на фронте нет. */ parentCode: string | null; lineStatus: LineStatus; /** порядковый номер строки в файле; null у достроенной и созданной правкой */ rowIndex: number | null; parentRowIndex: number | null; /** код как распознан в файле (например «ИЧ,250»), даже если `code` не распознан */ codeText: string | null; codeConfidence: string | null; needsReview: boolean; isConfirmed: boolean; codeModified: boolean; 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, referenceName: line.reference_name, origin: line.origin, parentCode: line.parent_code ?? null, lineStatus: line.line_status, rowIndex: line.row_index, parentRowIndex: line.parent_row_index, codeText: line.line_code_text, codeConfidence: line.line_code_confidence, needsReview: line.needs_review, isConfirmed: line.is_confirmed, codeModified: line.line_code_modified, cells, }; }); return { columns, rows }; } /** * Флаги ячейки: внимание — только по `needs_review` (уверенность решает ядро, сама по себе не подсвечивает); * подтверждённая ячейка не меняет уверенность, это отдельный флаг. */ export function cellFlags(value: ValueOut | undefined): { attention: boolean; modified: boolean; confirmed: boolean } { if (!value) { return { attention: false, modified: false, confirmed: false }; } return { attention: value.needs_review, modified: value.is_modified, confirmed: value.is_confirmed }; } 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, lineActions: 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) && !(row.lineId !== null && row.lineId in lineActions) && !Object.values(row.cells).some(hasValue), ) .map((row) => row.key), ); } /** Каноничный вид суммы для сравнения строк: без хвостовых нулей дроби и лишних ведущих нулей. */ 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 }; }