Andrey Epifancev 88a4920d7d fix(arm-web): нераспознанный текст суммы, пустые подстроки без расшифровок, свёрнутый список предупреждений (#43)
T11.5 valueCellText, T11.10 emptySublineKeys пропускает detail, T11.11 collapseIssues (3 записи + «Показать все»).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 20:06:02 +04:00

127 lines
5.3 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.

// Куда в интерфейсе положить каждую запись issues (P6-5): у строки, ячейки, колонки, поля шапки или у документа.
// Только показ: данные не меняются.
import type { ColumnOut, IssueOut, Severity } from '../../types/api';
import { columnTitle } from './reviewView';
import { type GridRow, cellKey } from './statementGrid';
export const HEADER_BLOCK_FIELDS = ['organization_name', 'inn', 'report_date', 'units'] as const;
export interface IssuePlaces {
byRow: Map<string, IssueOut[]>;
/** ключ — cellKey(rowKey, columnId) */
byCell: Map<string, IssueOut[]>;
byColumn: Map<string, IssueOut[]>;
byField: Map<string, IssueOut[]>;
document: IssueOut[];
}
export interface IssueListItem {
id: string;
severity: Severity;
message: string;
place: string;
targetKey: string | null;
}
const push = (map: Map<string, IssueOut[]>, key: string, issue: IssueOut) => {
const list = map.get(key);
if (list) list.push(issue);
else map.set(key, [issue]);
};
export function issuePlaces(
issues: IssueOut[],
rows: GridRow[],
columns: ColumnOut[],
fields: readonly string[] = HEADER_BLOCK_FIELDS,
): IssuePlaces {
const places: IssuePlaces = { byRow: new Map(), byCell: new Map(), byColumn: new Map(), byField: new Map(), document: [] };
const rowByLineId = new Map<string, GridRow>();
for (const r of rows) if (r.lineId !== null) rowByLineId.set(r.lineId, r);
const columnIds = new Set(columns.map((c) => c.id));
for (const issue of issues) {
const lineIds = issue.line_ids ?? [];
if (issue.field !== null) {
if (fields.includes(issue.field)) push(places.byField, issue.field, issue);
else places.document.push(issue);
continue;
}
const known = lineIds.map((id) => rowByLineId.get(id)).filter((r): r is GridRow => r !== undefined);
const columnId = issue.column_id !== null && columnIds.has(issue.column_id) ? issue.column_id : null;
if (lineIds.length > 0 && known.length > 0) {
for (const r of known) {
if (columnId !== null) push(places.byCell, cellKey(r.key, columnId), issue);
else push(places.byRow, r.key, issue);
}
} else if (lineIds.length === 0 && columnId !== null) {
push(places.byColumn, columnId, issue);
} else {
places.document.push(issue);
}
}
return places;
}
const FIELD_LABELS: Record<string, string> = {
organization_name: 'Организация',
inn: 'ИНН',
report_date: 'Дата отчёта',
units: 'Единица измерения',
};
const rowTitle = (row: GridRow): string => {
switch (row.lineStatus) {
case 'ok':
return `строка ${row.code ?? ''}`.trim();
case 'detail':
return `расшифровка «${row.name ?? ''}»`;
default: {
const text = row.codeText?.trim();
return text ? `строка без кода «${text}»` : 'строка без кода';
}
}
};
const SEVERITY_ORDER: Record<Severity, number> = { error: 0, info: 1 };
/** Список предупреждений документа: только kind = warning, сначала error, затем info, внутри — как пришло. */
export function issueListItems(issues: IssueOut[], places: IssuePlaces, rows: GridRow[], columns: ColumnOut[]): IssueListItem[] {
const rowByLineId = new Map<string, GridRow>();
for (const r of rows) if (r.lineId !== null) rowByLineId.set(r.lineId, r);
const columnById = new Map(columns.map((c) => [c.id, c]));
const inDocument = new Set(places.document);
const items = issues
.filter((issue) => issue.kind === 'warning')
.map((issue): IssueListItem => {
const first = (issue.line_ids ?? []).map((id) => rowByLineId.get(id)).find((r) => r !== undefined);
const column = issue.column_id !== null ? columnById.get(issue.column_id) : undefined;
let place: string;
let targetKey: string | null = null;
if (inDocument.has(issue)) {
place =
issue.column_index !== null && issue.column_id === null ? `столбец ${issue.column_index + 1} вне колонок формы` : 'документ';
} else if (issue.field !== null) {
place = `поле «${FIELD_LABELS[issue.field] ?? issue.field}»`;
} else if (first) {
targetKey = first.key;
place = column ? `${rowTitle(first)}, колонка «${columnTitle(column)}»` : rowTitle(first);
} else {
place = column ? `колонка «${columnTitle(column)}»` : 'документ';
}
return { id: issue.id, severity: issue.severity, message: issue.message, place, targetKey };
});
// сортировка устойчивая: порядок issues внутри уровня сохраняется
return items.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
}
export function worstSeverity(issues: IssueOut[]): Severity | null {
if (issues.length === 0) return null;
return issues.some((i) => i.severity === 'error') ? 'error' : 'info';
}
/** Свёрнутый список предупреждений (T11.11): первые `limit` (порядок уже «ошибки первыми») и сколько скрыто. */
export function collapseIssues<T>(items: T[], expanded: boolean, limit: number): { shown: T[]; hidden: number } {
if (expanded || items.length <= limit) return { shown: items, hidden: 0 };
return { shown: items.slice(0, limit), hidden: items.length - limit };
}