arm-web/src/widgets/DocumentReview/statementGrid.ts

132 lines
5.6 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 { 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<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,
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<string, string>,
lineActions: Record<string, LineAction> = {},
): 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) &&
!(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<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 };
}