AURORA-1893: плашка контроля ошибок со счётчиком, переходом к ячейке и подсказкой #283

Open
aep wants to merge 5 commits from feat/282-aurora-1893-error-alert-navigation into test
Owner

Closes #282.

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

Эта ветка стоит не на test, а поверх трёх незамерженных PR:

test → PR #277 (AURORA-1894, фикс красной рамки ошибки/тоста) → PR #278 (AURORA-1892, отрицательные суммы красным + nonNegativeRule, merge-коммит d518961) → PR #281 (AURORA-1890, переписанная редактируемая/нередактируемая/закрытая стилистика Cell.jsx через isCellEditable/isComputedColumn) → эта ветка.

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

Проблема

Отрицательные значения и в целом ошибки контроля были недостаточно заметны:

  • в списке ошибок была одна запись на колонку (без строки и без количества) — если в колонке 30 ошибочных строк, видна одна запись;
  • плашка «Контроль ошибок» — небольшая бледно-оранжевая кнопка без счётчика, в конце панели настроек;
  • клик по пункту списка вёл только к колонке (горизонтальный скролл + выделение колонки), не к строке/ячейке;
  • на самой ошибочной ячейке не было подсказки с причиной — только цвет.

Что сделано

  • utils/validationUtils.js — collectInvalidErrors теперь собирает ошибки по ЯЧЕЙКАМ: { id: "<rule.id>:<columnKey>:<lineId>", ruleId, columnId, lineId, rowLabel, message } (раньше — одна запись на правило+колонку, без строки). Новая groupValidationErrors(errors) группирует их обратно по правилу+колонке со счётчиком и списком ячеек — для меню плашки.
  • hooks/useValidationRules.js — добавлена getCellErrors(columnId, value, row), отдающая причины конкретной ячейки (для подсказки); isCellInvalid теперь выражена через неё. Сравнение стабильности errors упрощено до сравнения по id (он уже кодирует правило+колонку+строку, дополнительных полей для устойчивости ссылки не нужно).
  • RealtimeTable.jsx — handleNavigateToError({ lineId, columnId }): находит строку по getRowId в table.getRowModel().flatRows (см. рулинг ниже), раскрывает родителей через setExpanded, при необходимости включает скрытую настройками колонку, выставляет activeSearchMatch (обводка 3px, как в navigateToSearchMatch), двигает selectedColumnId/горизонтальный скролл через существующий handleNavigateToColumn, и скроллит виртуализированный список к строке через rowVirtualizerRef.
  • SettingPanel/ValidationErrorsAlert.jsx — красная плашка (#FDEDED фон, #D32F2F рамка/текст), иконка ErrorOutline, счётчик «Ошибок: N», кнопки «‹ ›» для прохода по всем ошибкам по очереди; меню сгруппировано по правилу+колонке (заголовок группы + до 20 строк, дальше «и ещё N»); клик по строке ведёт к конкретной ячейке. Плашка по-прежнему не показывается при 0 ошибок.
  • SettingPanel/SettingPanel.jsx — плашка перенесена в начало панели, перед группами настроек (было — в конце, рядом с легендой цветов); добавлены пропсы onNavigateToError/headerPaths.
  • tableColumns.jsx / Cell/Cell/Cell.jsx — getCachedCellProps собирает invalidMessage через getCellErrors и включает его в кэш/сравнение ячейки; Cell показывает причину во всплывающей подсказке (MUI Tooltip) при наведении на невалидную ячейку.

Тесты

node --test src/components/RealtimeTable/utils/validationUtils.test.js — 12/12 зелёных:

  • 5 существующих тестов nonNegativeRule/isValidationRuleEnabled из AURORA-1892 — не менялись, подтверждают отсутствие регрессии;
  • 7 новых: collectInvalidErrors даёт запись на каждую ошибочную ячейку с lineId/columnId, не дублирует при повторном вызове с тем же seen, заполняет rowLabel по цепочке data.header.name → data.name → номер строки; groupValidationErrors считает количество по правилу+колонке (и для пустого списка); appliesToRow (current-adjustments-zero) и errorColumnKeys (booking-less-balance) учитываются на реальных правилах из constants/validation.js.

Полный прогон node --test src/components/RealtimeTable/utils/*.test.js — 69/70 (единственный падающий — columnLayoutUtils.test.js, падает из-за отсутствующего node_modules/@tanstack/table-core в этом worktree-окружении, не связан с изменениями этого PR; та же причина уже отмечена в описании #278/#281, подтверждено повторным прогоном этого теста из основного чекаута, где он зелёный).

Ручная проверка — по коду (живой стенд не поднимал): форма с 10+ ошибками → счётчик и группировка по правилу/колонке в меню; клик по строке в середине таблицы → раскрытие свёрнутой родительской группы + скролл виртуализатора к строке + обводка ячейки; наведение на красную ячейку → подсказка с причиной.

Rulings (решения без отдельного обсуждения)

  • rowLabel: тикет описывает цепочку data.header.name || data.name || номер строки без указания формата числа. Использован row.sort_order (то же значение, что рендерится в колонке «№» — tableColumns.jsx), приведённое к строке, без дополнительного текста типа «Строка N».
  • Поиск строки для навигации — flatRows, а не rows: table.getRowModel().rows в этом стеке (paginateExpandedRows не выключен, дефолт true у @tanstack/table-core) реально отфильтровывает потомков свёрнутых групп (expandRows()), значит искать в нём строку, которую ещё нужно раскрыть, невозможно — она там просто не появится. flatRows того же getRowModel() сохраняет все строки независимо от expanded, поэтому именно он использован для начального поиска по lineId; scrollToIndex после раскрытия всё равно считается по актуальному rows (в requestAnimationFrame, как и navigateToSearchMatch).
  • Включение скрытой колонки: тикет называет handleToggleColumnVisibility ИЛИ тост. У handleToggleColumnVisibility (из useColumnSettings) сигнатура принимает узел дерева колонок (collectLeafColumns(column)), а не columnId — поэтому для программного включения по id использована handleSetColumnsVisibility({ [columnId]: true }), которая уже существует и принимает ровно {columnId: isVisible}. Выбрано включать колонку (не просто тост), чтобы клик по ошибке гарантированно показывал ячейку, как того требует критерий приёмки.
  • Сообщение ошибки без пути колонки: раньше collectInvalidErrors дописывал к rule.message суффикс «Ошибка в колонке: …» (из headerPaths), так как запись на колонку была одна и путь был единственным способом её различить. Теперь колонку показывает заголовок группы в меню плашки (headerPaths передан в ValidationErrorsAlert отдельным пропом), поэтому message ячейки — чистый rule.message, как и указано в тикете.
  • Плашка при 0 ошибок: обёрнута условием в SettingPanel.jsx (validationErrors.length > 0) вместе с разделителем Divider, чтобы не оставлять «осиротевший» разделитель в начале панели настроек, когда ошибок нет.

🤖 Generated with Claude Code

Closes #282. ## Стек веток (важно для ревью) Эта ветка стоит не на `test`, а поверх трёх незамерженных PR: `test` → PR #277 (AURORA-1894, фикс красной рамки ошибки/тоста) → PR #278 (AURORA-1892, отрицательные суммы красным + `nonNegativeRule`, merge-коммит `d518961`) → PR #281 (AURORA-1890, переписанная редактируемая/нередактируемая/закрытая стилистика `Cell.jsx` через `isCellEditable`/`isComputedColumn`) → эта ветка. Диф этого PR временно включает коммиты #277, #278 и #281 (они ещё не в `test`), как и #278/#281 сами документируют это для своих предшественников. PR создан с `--base test` по процессу; после мерджа #277/#278/#281 ревьюеру будет проще смотреть только изменения этой задачи (последний коммит, `feat(table): плашка контроля ошибок...`). ## Проблема Отрицательные значения и в целом ошибки контроля были недостаточно заметны: - в списке ошибок была одна запись на колонку (без строки и без количества) — если в колонке 30 ошибочных строк, видна одна запись; - плашка «Контроль ошибок» — небольшая бледно-оранжевая кнопка без счётчика, в конце панели настроек; - клик по пункту списка вёл только к колонке (горизонтальный скролл + выделение колонки), не к строке/ячейке; - на самой ошибочной ячейке не было подсказки с причиной — только цвет. ## Что сделано - **`utils/validationUtils.js`** — `collectInvalidErrors` теперь собирает ошибки по ЯЧЕЙКАМ: `{ id: "<rule.id>:<columnKey>:<lineId>", ruleId, columnId, lineId, rowLabel, message }` (раньше — одна запись на правило+колонку, без строки). Новая `groupValidationErrors(errors)` группирует их обратно по правилу+колонке со счётчиком и списком ячеек — для меню плашки. - **`hooks/useValidationRules.js`** — добавлена `getCellErrors(columnId, value, row)`, отдающая причины конкретной ячейки (для подсказки); `isCellInvalid` теперь выражена через неё. Сравнение стабильности `errors` упрощено до сравнения по `id` (он уже кодирует правило+колонку+строку, дополнительных полей для устойчивости ссылки не нужно). - **`RealtimeTable.jsx`** — `handleNavigateToError({ lineId, columnId })`: находит строку по `getRowId` в `table.getRowModel().flatRows` (см. рулинг ниже), раскрывает родителей через `setExpanded`, при необходимости включает скрытую настройками колонку, выставляет `activeSearchMatch` (обводка 3px, как в `navigateToSearchMatch`), двигает `selectedColumnId`/горизонтальный скролл через существующий `handleNavigateToColumn`, и скроллит виртуализированный список к строке через `rowVirtualizerRef`. - **`SettingPanel/ValidationErrorsAlert.jsx`** — красная плашка (`#FDEDED` фон, `#D32F2F` рамка/текст), иконка `ErrorOutline`, счётчик «Ошибок: N», кнопки «‹ ›» для прохода по всем ошибкам по очереди; меню сгруппировано по правилу+колонке (заголовок группы + до 20 строк, дальше «и ещё N»); клик по строке ведёт к конкретной ячейке. Плашка по-прежнему не показывается при 0 ошибок. - **`SettingPanel/SettingPanel.jsx`** — плашка перенесена в начало панели, перед группами настроек (было — в конце, рядом с легендой цветов); добавлены пропсы `onNavigateToError`/`headerPaths`. - **`tableColumns.jsx` / `Cell/Cell/Cell.jsx`** — `getCachedCellProps` собирает `invalidMessage` через `getCellErrors` и включает его в кэш/сравнение ячейки; `Cell` показывает причину во всплывающей подсказке (MUI `Tooltip`) при наведении на невалидную ячейку. ## Тесты `node --test src/components/RealtimeTable/utils/validationUtils.test.js` — **12/12 зелёных**: - 5 существующих тестов `nonNegativeRule`/`isValidationRuleEnabled` из AURORA-1892 — не менялись, подтверждают отсутствие регрессии; - 7 новых: `collectInvalidErrors` даёт запись на каждую ошибочную ячейку с `lineId`/`columnId`, не дублирует при повторном вызове с тем же `seen`, заполняет `rowLabel` по цепочке `data.header.name → data.name → номер строки`; `groupValidationErrors` считает количество по правилу+колонке (и для пустого списка); `appliesToRow` (`current-adjustments-zero`) и `errorColumnKeys` (`booking-less-balance`) учитываются на реальных правилах из `constants/validation.js`. Полный прогон `node --test src/components/RealtimeTable/utils/*.test.js` — 69/70 (единственный падающий — `columnLayoutUtils.test.js`, падает из-за отсутствующего `node_modules/@tanstack/table-core` в этом worktree-окружении, не связан с изменениями этого PR; та же причина уже отмечена в описании #278/#281, подтверждено повторным прогоном этого теста из основного чекаута, где он зелёный). Ручная проверка — по коду (живой стенд не поднимал): форма с 10+ ошибками → счётчик и группировка по правилу/колонке в меню; клик по строке в середине таблицы → раскрытие свёрнутой родительской группы + скролл виртуализатора к строке + обводка ячейки; наведение на красную ячейку → подсказка с причиной. ## Rulings (решения без отдельного обсуждения) - **rowLabel**: тикет описывает цепочку `data.header.name || data.name || номер строки` без указания формата числа. Использован `row.sort_order` (то же значение, что рендерится в колонке «№» — `tableColumns.jsx`), приведённое к строке, без дополнительного текста типа «Строка N». - **Поиск строки для навигации — `flatRows`, а не `rows`**: `table.getRowModel().rows` в этом стеке (`paginateExpandedRows` не выключен, дефолт `true` у `@tanstack/table-core`) реально отфильтровывает потомков свёрнутых групп (`expandRows()`), значит искать в нём строку, которую *ещё* нужно раскрыть, невозможно — она там просто не появится. `flatRows` того же `getRowModel()` сохраняет все строки независимо от `expanded`, поэтому именно он использован для начального поиска по `lineId`; `scrollToIndex` после раскрытия всё равно считается по актуальному `rows` (в `requestAnimationFrame`, как и `navigateToSearchMatch`). - **Включение скрытой колонки**: тикет называет `handleToggleColumnVisibility` ИЛИ тост. У `handleToggleColumnVisibility` (из `useColumnSettings`) сигнатура принимает узел дерева колонок (`collectLeafColumns(column)`), а не `columnId` — поэтому для программного включения по id использована `handleSetColumnsVisibility({ [columnId]: true })`, которая уже существует и принимает ровно `{columnId: isVisible}`. Выбрано включать колонку (не просто тост), чтобы клик по ошибке гарантированно показывал ячейку, как того требует критерий приёмки. - **Сообщение ошибки без пути колонки**: раньше `collectInvalidErrors` дописывал к `rule.message` суффикс «Ошибка в колонке: …» (из `headerPaths`), так как запись на колонку была одна и путь был единственным способом её различить. Теперь колонку показывает заголовок группы в меню плашки (`headerPaths` передан в `ValidationErrorsAlert` отдельным пропом), поэтому `message` ячейки — чистый `rule.message`, как и указано в тикете. - **Плашка при 0 ошибок**: обёрнута условием в `SettingPanel.jsx` (`validationErrors.length > 0`) вместе с разделителем `Divider`, чтобы не оставлять «осиротевший» разделитель в начале панели настроек, когда ошибок нет. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 5 commits 2026-11-01 14:13:52 +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>
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 зелёных.
# Conflicts:
#	web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
- utils/cellEditability.js: isCellEditable/isComputedColumn — чистые функции,
  isComputedColumn повторяет часть v3._apply_form_cell/_apply_form3_cell
  (0023_apply_cell_empty_string.sql), чтобы админ не видел вычисляемые
  колонки как редактируемые. tableColumns.jsx использует isCellEditable
  вместо инлайновой проверки.
- useRealtimeData.js: возвращает stageColumnDeadlines (closes_at по колонке
  текущего этапа) рядом с columnsCurStage.
- Cell.jsx: редактируемая ячейка — белый фон, рамка 1px #7FA7C9 (2px при
  наведении), курсор text; нередактируемая ячейка строки INPUT — фон
  #F4F5F7, текст #5F6B7A; занятая коллегой — пунктирная оранжевая рамка
  2px #F0A030 без изменения фона. Строки ITEM/GROUP/ROOT не затронуты.
  Сохранён порядок стилей из AURORA-1894 (INVALID_STYLES последним) и
  спред fontWeight для отрицательных чисел из AURORA-1892.
- TableHead.jsx: метка срока ввода «✎ до DD.MM HH:mm» в заголовке колонки
  текущего этапа, только для исполнителя (utils/deadlineFormat.js).
- SettingPanel/CellLegend.jsx: иконка "?" с легендой (можно вводить /
  расчёт или закрыто) рядом с плашкой контроля ошибок, без новой панели
  и без новых цветов в palettes columnColors.js.

node --test: cellEditability.test.js, deadlineFormat.test.js — 18/18.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- validationUtils.collectInvalidErrors теперь собирает ошибки по ЯЧЕЙКАМ
  (id/ruleId/columnId/lineId/rowLabel/message), а не одну запись на колонку;
  новая groupValidationErrors(errors) группирует их обратно по правилу+колонке
  со счётчиком для меню плашки.
- useValidationRules: getCellErrors(columnId, value, row) отдаёт причины
  конкретной ячейки (для подсказки), isCellInvalid выражен через него;
  сравнение стабильности errors — по id (он уже кодирует правило+колонку+строку).
- RealtimeTable: handleNavigateToError({lineId, columnId}) — находит строку по
  getRowId в table.getRowModel().flatRows (не .rows, т.к. flatRows не фильтруется
  по свёрнутым группам), раскрывает родителей через setExpanded, включает
  скрытую колонку через handleSetColumnsVisibility, выделяет ячейку как
  activeSearchMatch и скроллит к ней — по образцу navigateToSearchMatch.
- ValidationErrorsAlert: красная плашка (#FDEDED/#D32F2F) со счётчиком
  «Ошибок: N», кнопками ‹›  для прохода по всем ошибкам и меню, сгруппированным
  по правилу+колонке (до 20 строк, дальше «и ещё N»); перенесена в начало
  SettingPanel, перед группами настроек.
- tableColumns/Cell: invalidMessage (из getCellErrors) доходит до Cell и
  показывается подсказкой (MUI Tooltip) при наведении на невалидную ячейку.

Тесты (node --test .../validationUtils.test.js): 12/12, включая 5 существующих
тестов nonNegativeRule из AURORA-1892 (не менялись, подтверждают отсутствие
регрессии) и 7 новых для collectInvalidErrors/groupValidationErrors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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/282-aurora-1893-error-alert-navigation:feat/282-aurora-1893-error-alert-navigation
git checkout feat/282-aurora-1893-error-alert-navigation
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#283
No description provided.