Andrey Epifancev a61c09be18 fix(table): контроль ошибок — агрегатные строки, переход и прокрутка к ячейке, подстановка {year}/{unit} (AURORA-1893)
Доработка по возврату тестирования 06.11:
- у агрегатных строк (ROOT/GROUP/ITEM/SUB_ITEM) нет data.id, ключ ошибки был
  «правило:колонка:null» — все агрегаты одной колонки схлопывались в одну
  запись. Ключ строки теперь getValidationRowKey: line id или sort:<sort_order>;
- переход к ошибке ищет строку по тому же ключу (раньше для агрегата
  прерывался, выделение оставалось на предыдущей ячейке);
- прокрутка: вместо scrollToIndex(rowIndex + 5, 'auto') — центрирование и
  scrollIntoView самой ячейки после отрисовки (у последних строк запас +5
  обрезался, шапка внутри контейнера не учитывалась — ячейка оставалась ниже
  видимой области);
- названия колонок в плашке и в тосте EditCell — с подставленными годом и
  единицами (formatHeaderPaths, как в TableHead);
- рамка активной ячейки дублируется псевдоэлементом ::after: inset-тень td
  перекрывалась красной рамкой ошибочной ячейки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:38:49 +04:00

125 lines
5.3 KiB
JavaScript
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 { formatHeaderUnit, formatHeaderYear } from './headerYearUtils.js';
import { getDataRowId } from './rowUtils.js';
// Достаёт вложенное поле из объекта строки таблицы
export const getValueByPath = (row, path) => path.split('.').reduce((value, key) => value?.[key], row);
const matchesValidationScope = (scope, { formType, sheetName }) =>
(!scope.formType || scope.formType === formType) &&
(!scope.sheetName || scope.sheetName === sheetName);
// Пустой includedScopes означает «работает везде»; исключения приоритетнее.
export const isValidationRuleEnabled = (rule, context = {}) => {
const includedScopes = rule.includedScopes || [];
const excludedScopes = rule.excludedScopes || [];
if (excludedScopes.some((scope) => matchesValidationScope(scope, context))) return false;
if (includedScopes.length && !includedScopes.some((scope) => matchesValidationScope(scope, context))) return false;
return true;
};
// Строит пути заголовков колонок
export const buildColumnHeaderPaths = (columns = [], path = [], map = {}) => {
for (const column of columns) {
const nextPath = column.header ? [...path, column.header] : path;
if (column.columns?.length) {
buildColumnHeaderPaths(column.columns, nextPath, map);
} else if (column.accessorKey) {
map[column.accessorKey] = nextPath;
}
}
return map;
};
// AURORA-1893 (возврат 06.11): заголовки колонок в конфигах содержат плейсхолдеры
// {year}/{year:short}/{unit} — подставляем их так же, как TableHead, чтобы в «Контроле
// ошибок» и в тосте EditCell не было «План {year} ({unit})».
export const formatHeaderPaths = (headerPaths, budgetYear, denomination) => {
const formatted = {};
for (const [columnId, path] of Object.entries(headerPaths || {})) {
formatted[columnId] = path.map((segment) => formatHeaderUnit(formatHeaderYear(segment, budgetYear), denomination));
}
return formatted;
};
// AURORA-1893 (возврат 06.11): ключ строки для ошибки и перехода к ней. У строк ввода
// это line id, а у агрегатных строк (ROOT/GROUP/ITEM/SUB_ITEM) data.id нет — раньше ключ
// у всех агрегатов одной колонки был null, они схлопывались в одну запись и переход к ним
// не срабатывал. sort_order — глобальный ROW_NUMBER листа, уникален для каждой строки.
export const getValidationRowKey = (row) => {
const lineId = getDataRowId(row?.data);
if (lineId !== null && lineId !== undefined) return lineId;
if (row?.sort_order !== undefined && row?.sort_order !== null) return `sort:${row.sort_order}`;
return null;
};
// AURORA-1893: человекочитаемая подпись строки для меню плашки ошибок —
// название статьи/ВСП, если есть, иначе номер строки (колонка «№»).
const getRowLabel = (row) => {
const data = row?.data;
if (data?.header?.name) return data.header.name;
if (data?.name) return data.name;
if (row?.sort_order !== undefined && row?.sort_order !== null) return String(row.sort_order);
return String(getDataRowId(data) ?? '');
};
// Обходит строки и собирает ошибки по правилу валидации — по записи на каждую
// ошибочную ЯЧЕЙКУ (не одна запись на колонку, как было раньше), чтобы плашка
// могла показать счётчик и перейти к конкретной строке.
export const collectInvalidErrors = (rows, rule, errors, seen) => {
for (const row of rows) {
if (!rule.appliesToRow || rule.appliesToRow(row)) {
for (const columnKey of rule.errorColumnKeys || rule.columnKeys) {
if (!rule.isInvalid(getValueByPath(row, columnKey), row, columnKey)) continue;
const lineId = getDataRowId(row.data);
const rowKey = getValidationRowKey(row);
const errorId = `${rule.id}:${columnKey}:${rowKey}`;
if (seen.has(errorId)) continue;
seen.add(errorId);
errors.push({
id: errorId,
ruleId: rule.id,
columnId: columnKey,
lineId,
rowKey,
rowLabel: getRowLabel(row),
message: rule.message,
});
}
}
collectInvalidErrors(row.subRows || [], rule, errors, seen);
}
};
// AURORA-1893: группирует ошибки по «правило + колонка» (как раньше отображалось
// в плашке) и прикладывает к каждой группе список конкретных ячеек со счётчиком.
export const groupValidationErrors = (errors) => {
const groupsByKey = new Map();
for (const error of errors) {
const key = `${error.ruleId}:${error.columnId}`;
let group = groupsByKey.get(key);
if (!group) {
group = {
id: key,
ruleId: error.ruleId,
columnId: error.columnId,
message: error.message,
cells: [],
count: 0,
};
groupsByKey.set(key, group);
}
group.cells.push(error);
group.count += 1;
}
return [...groupsByKey.values()];
};