Доработка по возврату тестирования 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>
125 lines
5.3 KiB
JavaScript
125 lines
5.3 KiB
JavaScript
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()];
|
||
};
|