// Куда в интерфейсе положить каждую запись 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; /** ключ — cellKey(rowKey, columnId) */ byCell: Map; byColumn: Map; byField: Map; document: IssueOut[]; } export interface IssueListItem { id: string; severity: Severity; message: string; place: string; targetKey: string | null; } const push = (map: Map, 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(); 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 = { 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 = { error: 0, info: 1 }; /** Список предупреждений документа: только kind = warning, сначала error, затем info, внутри — как пришло. */ export function issueListItems(issues: IssueOut[], places: IssuePlaces, rows: GridRow[], columns: ColumnOut[]): IssueListItem[] { const rowByLineId = new Map(); 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(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 }; }