AURORA-1892: отрицательные суммы красным, правило «минус запрещён» для заданных колонок #278

Open
aep wants to merge 3 commits from feat/276-aurora-1892-negative-amounts into test
Owner

Closes #276.

Стек веток (важно для ревью)

Эта ветка стоит не на test, а поверх незамерженного PR #277 (AURORA-1894, фикс красной рамки ошибки и тоста):
test → PR #277 → эта ветка (merge-коммит d518961, не rebase — конфликт в Cell.jsx разрешён руками: порядок стилей из #277 сохранён (INVALID_STYLES последним), строка ...(isNegative ? { fontWeight: 600 } : {}) добавлена перед ним).

Диф этого PR временно включает коммит #277 (он ещё не в test). PR создан с --base test по процессу; после мерджа #277 ревьюеру будет проще смотреть только изменения этой задачи.

Проблема

Отрицательные значения в таблице отображались тем же цветом, что и положительные (только яркость фона влияла на цвет текста), и ноль/минус визуально путались. Общего правила «в этой колонке минус запрещён» не было — существующие правила контроля (AURORA-1225) покрывали только частные случаи (остаток после брони, текущие корректировки = 0 и т.п.).

Что сделано

• web/src/components/RealtimeTable/utils/numberFormat.js (новый) — formatNumber вынесен из Cell.jsx, возвращает { text, isNegative }.
• Cell.jsx — отрицательные числа: #C62828 + font-weight: 600 на светлом фоне, #FFCDD2 на тёмном (яркость фона < 128, например ROOT красной палитры #933634). Ноль остаётся «-». Порядок спредов INVALID_STYLES/EDITABLE_STYLES не трогал — это зона параллельной ветки #275 (AURORA-1894), сохранён при слиянии.
• constants/validation.js — фабрика nonNegativeRule({ id, columnKeys, message, includedScopes, excludedScopes }) и общий список nonNegativeColumnKeys (план, бронь на 2026/2027, факт по месяцам и СПОД, сумма платежа, предельная стоимость договора/ЦКК, сателлиты АХР Аренда/Коммунальные услуги/Безопасность — факт по месяцам и квартальный итог, АХР Лимиты целиком). Корректировки и переносы (adj_, base_correction, revision_, seq_, target_change, transfer_) не включены — там минус — нормальная операция.
• Правило подключено в validationRules — подсветка ячейки и запись в «Контроль ошибок» работают через существующий useValidationRules/isCellInvalid, без изменения механики. Сохранение не блокируется (мягкий запрет).
• EditCell.jsx (handleSave) — при сохранении отрицательного числа в защищённой колонке показывается toast.warning('В колонке «<заголовок>» отрицательные суммы запрещены'), значение всё равно сохраняется. Заголовок колонки берётся из headerPaths, который useValidationRules теперь также возвращает (прокинут через RealtimeTable.jsx → table.options.meta.getColumnTitle).

Тесты

node --test src/components/RealtimeTable/utils/numberFormat.test.js src/components/RealtimeTable/utils/validationUtils.test.js src/components/RealtimeTable/utils/wsErrorUtils.test.js — 20/20 зелёных (проверено повторно после слияния с #277, включая тесты #277 для wsErrorUtils).
Полный прогон node --test src/components/RealtimeTable/utils/*.test.js — 34/35 (один сторонний тест, columnLayoutUtils.test.js, падает из-за отсутствующего node_modules/@tanstack/table-core в этом окружении — не связан с изменениями этого PR).
Ручная проверка — по коду (живой стенд не поднимал): -100 в data.plan.q1 → красный текст, подсветка, запись в «Контроль ошибок», toast при сохранении; -100 в data.q2.adj_current (корректировка) → только красный текст, без ошибки и без toast.

Какой перечень колонок использован

Запрошенное вложение Салмина М.П. (AURORA-1892_Перечень_колонок.md, attachment #2390389) даёт номера столбцов как на скриншоте заказчика (не буквы Excel), без самого скриншота. Сопоставить эти номера с кодовыми accessorKey без изображения нельзя надёжно (см. рулинг R-1). Поэтому базой взят собственный список тикета (data.plan., data.booking.y2026/2027., data.qN.actual_m1-3, data.qN.actual_spod, data.qN.pay_amount, data.contract.ceiling/data.ckk.ceiling/data.contract_summary.total, сателлиты), уточнённый одним конкретным качественным сигналом из вложения — для АХР Лимитов явно сказано «вообще не должно быть минусов» по всем числовым показателям листа, это шире исходного qty_qN/amount_qN и так и реализовано (см. рулинг R-2).

Открыто для человека (владелец задачи уже знает, 2026-11-01): нужен реальный перечень колонок (не дефолт тикета) и решение, распространять ли правило на форму 4 — сейчас правило подключено без includedScopes и формально задевает и форму 4, хотя вложение Салмина явно просит не переносить на неё автоматически перечни форм 1/2.

Rulings

• R-1: Вложение Салмина нумерует колонки по скриншоту заказчика (сам PNG не передан вместе с вложением), сопоставить номера с accessorKey в коде без изображения нельзя. Для основных листов (Операц/АХР/КВ ф.1/2/4) и сателлитов Аренда/Коммунальные/Безопасность использован собственный список тикета (семейства plan, booking.y2026/2027, actual_m1-3/actual_spod, pay_amount, ceiling), т.к. это единственный однозначный, привязанный к коду вариант. Рекомендую человеку свериться со скриншотом заказчика перед финальным приёмом.
• R-2: Для листа АХР Лимиты расширил перечень с qty_qN/amount_qN (дефолт тикета) до всех числовых полей листа (+limit_with_vat, limit_without_vat) — по явной качественной формулировке вложения «вообще не должно быть минусов».
• R-3: В ф.1 колонка предельной стоимости договора называется data.contract_detail.ceiling (а не data.contract.ceiling, как в ф.2/ф.4) — добавил оба ключа в список, это один и тот же показатель («предельная стоимость договора») под разными именами в разных формах.
• R-4: Для toast-предупреждения в EditCell.handleSave понадобился человекочитаемый заголовок колонки — useValidationRules теперь дополнительно возвращает headerPaths (уже вычислялся внутри хука), RealtimeTable.jsx прокидывает его в table.options.meta.getColumnTitle. Минимальное изменение контракта хука, обратной совместимости не нарушает (поле добавлено, а не изменено).
• R-5: Жёсткий запрет (блокировка сохранения) не делается — по тексту тикета это отдельная backend-задача, которую заказчик пока не подтвердил.
• R-6: Ветка перебазирована через merge (не rebase) на #277 после создания PR, по решению пользователя — организовать 1894/1892/1890/1893 в цепочку, чтобы следующие задачи (AURORA-1890, AURORA-1893), тоже правящие Cell.jsx, не конфликтовали со всеми тремя предыдущими одновременно.

🤖 Generated with Claude Code

Closes #276. ## Стек веток (важно для ревью) Эта ветка стоит не на `test`, а поверх незамерженного PR #277 (AURORA-1894, фикс красной рамки ошибки и тоста): `test` → PR #277 → эта ветка (merge-коммит `d518961`, не rebase — конфликт в `Cell.jsx` разрешён руками: порядок стилей из #277 сохранён (`INVALID_STYLES` последним), строка `...(isNegative ? { fontWeight: 600 } : {})` добавлена перед ним). Диф этого PR временно включает коммит #277 (он ещё не в `test`). PR создан с `--base test` по процессу; после мерджа #277 ревьюеру будет проще смотреть только изменения этой задачи. ## Проблема Отрицательные значения в таблице отображались тем же цветом, что и положительные (только яркость фона влияла на цвет текста), и ноль/минус визуально путались. Общего правила «в этой колонке минус запрещён» не было — существующие правила контроля (AURORA-1225) покрывали только частные случаи (остаток после брони, текущие корректировки = 0 и т.п.). ## Что сделано • web/src/components/RealtimeTable/utils/numberFormat.js (новый) — formatNumber вынесен из Cell.jsx, возвращает { text, isNegative }. • Cell.jsx — отрицательные числа: #C62828 + font-weight: 600 на светлом фоне, #FFCDD2 на тёмном (яркость фона < 128, например ROOT красной палитры #933634). Ноль остаётся «-». Порядок спредов INVALID_STYLES/EDITABLE_STYLES не трогал — это зона параллельной ветки #275 (AURORA-1894), сохранён при слиянии. • constants/validation.js — фабрика nonNegativeRule({ id, columnKeys, message, includedScopes, excludedScopes }) и общий список nonNegativeColumnKeys (план, бронь на 2026/2027, факт по месяцам и СПОД, сумма платежа, предельная стоимость договора/ЦКК, сателлиты АХР Аренда/Коммунальные услуги/Безопасность — факт по месяцам и квартальный итог, АХР Лимиты целиком). Корректировки и переносы (adj_*, base_correction, revision_*, seq_*, target_change, transfer_*) не включены — там минус — нормальная операция. • Правило подключено в validationRules — подсветка ячейки и запись в «Контроль ошибок» работают через существующий useValidationRules/isCellInvalid, без изменения механики. Сохранение не блокируется (мягкий запрет). • EditCell.jsx (handleSave) — при сохранении отрицательного числа в защищённой колонке показывается toast.warning('В колонке «<заголовок>» отрицательные суммы запрещены'), значение всё равно сохраняется. Заголовок колонки берётся из headerPaths, который useValidationRules теперь также возвращает (прокинут через RealtimeTable.jsx → table.options.meta.getColumnTitle). ## Тесты node --test src/components/RealtimeTable/utils/numberFormat.test.js src/components/RealtimeTable/utils/validationUtils.test.js src/components/RealtimeTable/utils/wsErrorUtils.test.js — 20/20 зелёных (проверено повторно после слияния с #277, включая тесты #277 для wsErrorUtils). Полный прогон node --test src/components/RealtimeTable/utils/*.test.js — 34/35 (один сторонний тест, columnLayoutUtils.test.js, падает из-за отсутствующего node_modules/@tanstack/table-core в этом окружении — не связан с изменениями этого PR). Ручная проверка — по коду (живой стенд не поднимал): -100 в data.plan.q1 → красный текст, подсветка, запись в «Контроль ошибок», toast при сохранении; -100 в data.q2.adj_current (корректировка) → только красный текст, без ошибки и без toast. ## Какой перечень колонок использован Запрошенное вложение Салмина М.П. (AURORA-1892_Перечень_колонок.md, attachment #2390389) даёт номера столбцов как на скриншоте заказчика (не буквы Excel), без самого скриншота. Сопоставить эти номера с кодовыми accessorKey без изображения нельзя надёжно (см. рулинг R-1). Поэтому базой взят собственный список тикета (data.plan.*, data.booking.y2026/2027.*, data.qN.actual_m1-3, data.qN.actual_spod, data.qN.pay_amount, data.contract.ceiling/data.ckk.ceiling/data.contract_summary.total, сателлиты), уточнённый одним конкретным качественным сигналом из вложения — для АХР Лимитов явно сказано «вообще не должно быть минусов» по всем числовым показателям листа, это шире исходного qty_qN/amount_qN и так и реализовано (см. рулинг R-2). **Открыто для человека (владелец задачи уже знает, 2026-11-01): нужен реальный перечень колонок (не дефолт тикета) и решение, распространять ли правило на форму 4 — сейчас правило подключено без includedScopes и формально задевает и форму 4, хотя вложение Салмина явно просит не переносить на неё автоматически перечни форм 1/2.** ## Rulings • **R-1**: Вложение Салмина нумерует колонки по скриншоту заказчика (сам PNG не передан вместе с вложением), сопоставить номера с accessorKey в коде без изображения нельзя. Для основных листов (Операц/АХР/КВ ф.1/2/4) и сателлитов Аренда/Коммунальные/Безопасность использован собственный список тикета (семейства plan, booking.y2026/2027, actual_m1-3/actual_spod, pay_amount, ceiling), т.к. это единственный однозначный, привязанный к коду вариант. Рекомендую человеку свериться со скриншотом заказчика перед финальным приёмом. • **R-2**: Для листа АХР Лимиты расширил перечень с qty_qN/amount_qN (дефолт тикета) до всех числовых полей листа (+limit_with_vat, limit_without_vat) — по явной качественной формулировке вложения «вообще не должно быть минусов». • **R-3**: В ф.1 колонка предельной стоимости договора называется data.contract_detail.ceiling (а не data.contract.ceiling, как в ф.2/ф.4) — добавил оба ключа в список, это один и тот же показатель («предельная стоимость договора») под разными именами в разных формах. • **R-4**: Для toast-предупреждения в EditCell.handleSave понадобился человекочитаемый заголовок колонки — useValidationRules теперь дополнительно возвращает headerPaths (уже вычислялся внутри хука), RealtimeTable.jsx прокидывает его в table.options.meta.getColumnTitle. Минимальное изменение контракта хука, обратной совместимости не нарушает (поле добавлено, а не изменено). • **R-5**: Жёсткий запрет (блокировка сохранения) не делается — по тексту тикета это отдельная backend-задача, которую заказчик пока не подтвердил. • **R-6**: Ветка перебазирована через merge (не rebase) на #277 после создания PR, по решению пользователя — организовать 1894/1892/1890/1893 в цепочку, чтобы следующие задачи (AURORA-1890, AURORA-1893), тоже правящие Cell.jsx, не конфликтовали со всеми тремя предыдущими одновременно. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 1 commit 2026-11-01 11:21:23 +03:00
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 зелёных.
aep added 2 commits 2026-11-01 11:42:50 +03:00
Cell.jsx: порядок спреда стилей — base → editable/not-editable → locked →
invalid (INVALID_STYLES последним), чтобы красная рамка 2px не перетиралась
EDITABLE_STYLES у вводимых ячеек с ошибкой.

utils/wsErrorUtils.js: getWsErrorMessage(error) — строка возвращается как
есть, объект берёт message || detail, известные технические префиксы
(computed_field:, role_not_allowed:, window_closed:, org_not_assigned)
заменяются понятным текстом, прочее отдаётся без "Server error:".

RealtimeContext.js: toast.error(getWsErrorMessage(data.error)) вместо
`Server error:${data.error.message}` || '', которое всегда давало undefined
для ошибок-строк (|| не срабатывает на непустой строке).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/276-aurora-1892-negative-amounts:feat/276-aurora-1892-negative-amounts
git checkout feat/276-aurora-1892-negative-amounts
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/DFiP_Budget_planing#278
No description provided.