Cell.jsx: formatNumber вынесен в utils/numberFormat.js и возвращает
{ text, isNegative }; отрицательные числа выделяются цветом (#C62828 на
светлом фоне, #FFCDD2 на тёмном) и font-weight 600, ноль остаётся «-».
constants/validation.js: фабрика nonNegativeRule и общий список
nonNegativeColumnKeys (план, бронь на 2026/2027, факт по месяцам и СПОД,
сумма платежа, предельная стоимость договора/ЦКК, сателлиты АХР
Аренда/Коммунальные/Безопасность, АХР Лимиты целиком). Корректировки и
переносы (adj_*, base_correction, revision_*, seq_*, target_change,
transfer_*) не включены — там минус — нормальная операция. Запрет
мягкий: подсветка и запись в «Контроль ошибок» через существующий
useValidationRules/isCellInvalid, сохранение не блокируется.
EditCell.jsx: handleSave предупреждает toast.warning при сохранении
отрицательного числа в защищённой колонке, значение всё равно
сохраняется. useValidationRules и RealtimeTable прокидывают
человекочитаемый заголовок колонки для текста предупреждения.
Тесты (node --test): numberFormat.test.js, validationUtils.test.js —
новые, 10/10 зелёных.
53 lines
1.7 KiB
JavaScript
53 lines
1.7 KiB
JavaScript
import { useCallback, useMemo, useRef } from 'react';
|
|
import { validationRules } from '../constants/validation';
|
|
import { buildColumnHeaderPaths, collectInvalidErrors, isValidationRuleEnabled } from '../utils/validationUtils';
|
|
|
|
const EMPTY_ARRAY = [];
|
|
|
|
export const useValidationRules = (data, columns, formType, sheetName) => {
|
|
const safeData = data || EMPTY_ARRAY;
|
|
const safeColumns = columns || EMPTY_ARRAY;
|
|
const stableErrorsRef = useRef([]);
|
|
const headerPaths = useMemo(() => buildColumnHeaderPaths(safeColumns), [safeColumns]);
|
|
const activeRules = useMemo(
|
|
() => validationRules.filter((rule) => isValidationRuleEnabled(rule, { formType, sheetName })),
|
|
[formType, sheetName],
|
|
);
|
|
|
|
const calculatedErrors = useMemo(() => {
|
|
const result = [];
|
|
const seen = new Set();
|
|
|
|
for (const rule of activeRules) {
|
|
collectInvalidErrors(safeData, rule, headerPaths, result, seen);
|
|
}
|
|
|
|
return result;
|
|
}, [activeRules, headerPaths, safeData]);
|
|
|
|
const previousErrors = stableErrorsRef.current;
|
|
const errorsAreEqual =
|
|
previousErrors.length === calculatedErrors.length &&
|
|
previousErrors.every(
|
|
(error, index) =>
|
|
error.id === calculatedErrors[index].id &&
|
|
error.columnId === calculatedErrors[index].columnId &&
|
|
error.message === calculatedErrors[index].message,
|
|
);
|
|
if (!errorsAreEqual) {
|
|
stableErrorsRef.current = calculatedErrors;
|
|
}
|
|
const errors = stableErrorsRef.current;
|
|
|
|
const isCellInvalid = useCallback(
|
|
(columnId, value, row) =>
|
|
activeRules.some(
|
|
(rule) =>
|
|
rule.columnKeys.includes(columnId) && (!rule.appliesToRow || rule.appliesToRow(row)) && rule.isInvalid(value, row, columnId),
|
|
),
|
|
[activeRules],
|
|
);
|
|
|
|
return { errors, isCellInvalid, headerPaths };
|
|
};
|