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
127 lines
5.3 KiB
TypeScript
127 lines
5.3 KiB
TypeScript
// Куда в интерфейсе положить каждую запись 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 };
|
||
}
|