AURORA-1890: индикация редактируемых/расчётных ячеек и срок ввода в заголовке #281

Open
aep wants to merge 4 commits from feat/280-aurora-1890-cell-editability into test
Owner

Closes #280.

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

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

test → PR #277 (AURORA-1894, фикс красной рамки ошибки/тоста) → PR #278 (AURORA-1892, отрицательные суммы красным, merge-коммит d518961) → эта ветка.

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

Cell.jsx переписан (редактируемая/нередактируемая/занятая стилистика), но оба инварианта предыдущих веток сохранены:

  • из #277: INVALID_STYLES применяется последним в спреде стилей — красная рамка ошибки всегда выигрывает;
  • из #278: ...(isNegative ? { fontWeight: 600 } : {}) и связанный formatNumber/isNegative остались на своих местах.

Проблема

Пользователь не может на глаз отличить ячейку, доступную для ввода, от расчётной или закрытой на этапе — обе были белыми, разница была только в курсоре и полупрозрачной рамке 2px #d8e4ea94. Админ видел вычисляемые колонки как редактируемые (isEditable всегда true на INPUT) и получал ошибку сервера при попытке ввода. Ячейка, занятая коллегой, была похожа на readonly (серый фон). В заголовке колонки не было видно срока ввода.

Что сделано

  • web/src/components/RealtimeTable/utils/cellEditability.js (новый) — чистые функции isCellEditable({ rowType, columnId, isAdmin, stageColumns }) и isComputedColumn(columnId). isComputedColumn повторяет часть серверной проверки v3._apply_form_cell/v3._apply_form3_cell (api/alembic/versions/sql/0023_apply_cell_empty_string.sql, актуальна через CREATE OR REPLACE): approved.*/totals.*, header.* кроме name/internal_order/justification/vsp_id, набор полей кварталов (corrected_plan, new_plan, actual_quarter, booking, rem_booking, rem_actual, residual_after_booking, residual_after_actual, economy, total — и их аналоги quarter_actual/total_corr в FORM_3), *.year, сателлиты (fact_qN.total, address/object_type/rented_area) и лист лимитов (amount_qN, limit_with_vat/limit_without_vat). Список зашит в коде, с тестом.
  • web/src/components/RealtimeTable/tableColumns.jsx — isEditable внутри getTableColumns теперь вызывает isCellEditable, сигнатура не менялась (важно для AURORA-1891).
  • web/src/components/RealtimeTable/hooks/useRealtimeData.js — хук больше не отбрасывает closes_at: возвращает stageColumnDeadlines (Map(columnId → closes_at)) рядом с columnsCurStage.
  • web/src/components/RealtimeTable/Cell/Cell/Cell.jsx — без новых цветов в палитрах (constants/columnColors.js не менялся):
    • редактируемая ячейка строки INPUT: белый фон, рамка 1px solid #7FA7C9 (2px при наведении через локальный isHovered state — pseudo-класс :hover недоступен для inline-стилей), курсор text;
    • нередактируемая ячейка строки INPUT (вычисляемая или вне этапа): фон #F4F5F7, текст #5F6B7A, курсор default; строки ITEM/GROUP/ROOT не тронуты;
    • занятая другим пользователем: фон не меняется, рамка 2px dashed #F0A030 + значок 🔒 (раньше — серый фон #f5f5f5, визуально похоже на read-only);
    • порядок спредов сохранён: isNegative fontWeight → editable/non-editable → isLocked → isInvalid (последний, см. выше).
  • web/src/components/RealtimeTable/utils/deadlineFormat.js (новый) — formatDeadline(closesAt) → «до DD.MM HH:mm» в локальном времени браузера.
  • web/src/components/RealtimeTable/TableHead/TableHead.jsx — HeaderCell получает stageColumnDeadlines/userRoleId (прокинуты из RealtimeTable.jsx) и показывает метку ✎ до 31.10 18:00 под заголовком колонки текущего этапа, только исполнителю (не админу).
  • web/src/components/RealtimeTable/SettingPanel/CellLegend.jsx (новый) — иконка «?» с Tooltip из двух образцов («можно вводить» / «расчёт или закрыто на этапе»), подключена рядом с плашкой «Контроль ошибок» в SettingPanel.jsx, без отдельной панели.
  • Режим «только текущие колонки» (AURORA-1889) не трогал — использует тот же columnsCurStage, его форма не изменилась.

Тесты

node --test src/components/RealtimeTable/utils/cellEditability.test.js src/components/RealtimeTable/utils/deadlineFormat.test.js — 18/18 зелёных:

  • cellEditability.test.js (14 тестов) — все примеры из тикета (админ + data.q1.corrected_plan/data.approved.q1/data.totals.fact_year/data.header.vsp_address → false; админ + data.plan.q1 → true; исполнитель вне этапа → false; INPUT+этап → true; ITEM → false) плюс покрытие каждого правила isComputedColumn (qN-поля, FORM_3 quarter_actual/total_corr, *.year, сателлиты, лист лимитов).
  • deadlineFormat.test.js (4 теста) — формат «до DD.MM HH:mm», паддинг нулями, Date-инстанс напрямую, falsy/invalid → null.

Полный node --test src/components/RealtimeTable/utils/*.test.js — 62/63 (единственный сторонний фейл, columnLayoutUtils.test.js, из-за отсутствующего node_modules/@tanstack/table-core в этом worktree — не связан с этим PR, воспроизводится и на базовом коммите без моих изменений).

Линт: biome check на изменённых файлах не добавляет новых находок к существующему состоянию веток #277/#278/test (сверено diff-ом error/warning-счётчиков до и после изменения каждого файла); единственная находка, которую я внёс сам (nested ternary в новой логике Cell.jsx), исправлена до коммита.

Ручная проверка — по коду (живой стенд не поднимал): формы 1/2/4, сателлиты, лист проекта — исполнитель в открытом окне видит белые редактируемые ячейки с синей рамкой и метку срока в заголовке; исполнитель без окна и админ н�� вычисляемых колонках видят серые ячейки без рамки; админ не видит метку срока ввода.

Rulings

  • Текст деадлайна/курсор наведения через React state, не CSS. У ячеек нет своего stylesheet-класса (className='cell' — просто маркер, в проекте нет .cell CSS-правил), поэтому :hover для утолщения рамки 1px→2px реализован через локальный isHovered (onMouseEnter/onMouseLeave), а не псевдокласс.
  • Локальное время текста метки через Date, а не date-fns. В проекте уже есть date-fns, но соседние утилиты RealtimeTable/utils (headerYearUtils.js, web/src/utils/formatDate.js) форматируют даты через чистый Date/padStart без доп. зависимостей — для единообразия deadlineFormat.js сделан так же.
  • Текст пробрасывается как Map, а не массив. Сигнатура isCellEditable в тикете использует stageColumns.has(columnId), поэтому tableColumns.jsx строит Set из columnsCurStage (массива) один раз на пересборку колонок; useRealtimeData.js для stageColumnDeadlines сразу возвращает Map, так как TableHead.jsx делает точечные .get(columnId) по одной колонке на рендер хедер-ячейки, а не проверку принадлежности множеству.
  • data.totals.go_balance_only (OTCH9F) помечен как computed, хотя в SQL (_apply_form_cell) это поле редактируемое. Тикет явно требует: «scope approved.* и totals.* — всегда вычисляемые» без исключений — это сознательное упрощение фронтового списка (см. п.2 тикета: «Список зашит в коде... это фронтовый список с тестом»), один известный false positive для одного поля одного сателлита, не исправлял за рамками тикета.
  • Текст метки — убрал серый цвет текста #999999 у занятой ячейки. Тикет описывает только рамку/бейдж для locked-ячейки («фон ячейки не менять, рамка 2px dashed #F0A030 и значок 🔒»), но старый код также красил текст в серый, что усиливало прежнее сходство с read-only — убрал, оставил обычный brightness-based цвет текста, чтобы отличие было только в рамке/бейдже, как в критерии приёмки («отличается от нередактируемой»).
Closes #280. ## Стек веток (важно для ревью) Эта ветка стоит не на test, а поверх двух незамерженных PR: test → PR #277 (AURORA-1894, фикс красной рамки ошибки/тоста) → PR #278 (AURORA-1892, отрицательные суммы красным, merge-коммит `d518961`) → эта ветка. Диф этого PR временно включает коммиты #277 и #278 (они ещё не в test), как и #278 сам документирует это для #277. PR создан с `--base test` по процессу; после мерджа #277/#278 ревьюеру будет проще смотреть только изменения этой задачи. Cell.jsx переписан (редактируемая/нередактируемая/занятая стилистика), но оба инварианта предыдущих веток сохранены: - из #277: `INVALID_STYLES` применяется последним в спреде стилей — красная рамка ошибки всегда выигрывает; - из #278: `...(isNegative ? { fontWeight: 600 } : {})` и связанный `formatNumber`/`isNegative` остались на своих местах. ## Проблема Пользователь не может на глаз отличить ячейку, доступную для ввода, от расчётной или закрытой на этапе — обе были белыми, разница была только в курсоре и полупрозрачной рамке 2px #d8e4ea94. Админ видел вычисляемые колонки как редактируемые (isEditable всегда true на INPUT) и получал ошибку сервера при попытке ввода. Ячейка, занятая коллегой, была похожа на readonly (серый фон). В заголовке колонки не было видно срока ввода. ## Что сделано - `web/src/components/RealtimeTable/utils/cellEditability.js` (новый) — чистые функции `isCellEditable({ rowType, columnId, isAdmin, stageColumns })` и `isComputedColumn(columnId)`. `isComputedColumn` повторяет часть серверной проверки `v3._apply_form_cell`/`v3._apply_form3_cell` (`api/alembic/versions/sql/0023_apply_cell_empty_string.sql`, актуальна через `CREATE OR REPLACE`): `approved.*`/`totals.*`, `header.*` кроме name/internal_order/justification/vsp_id, набор полей кварталов (corrected_plan, new_plan, actual_quarter, booking, rem_booking, rem_actual, residual_after_booking, residual_after_actual, economy, total — и их аналоги quarter_actual/total_corr в FORM_3), `*.year`, сателлиты (fact_qN.total, address/object_type/rented_area) и лист лимитов (amount_qN, limit_with_vat/limit_without_vat). Список зашит в коде, с тестом. - `web/src/components/RealtimeTable/tableColumns.jsx` — `isEditable` внутри `getTableColumns` теперь вызывает `isCellEditable`, сигнатура не менялась (важно для AURORA-1891). - `web/src/components/RealtimeTable/hooks/useRealtimeData.js` — хук больше не отбрасывает `closes_at`: возвращает `stageColumnDeadlines` (`Map(columnId → closes_at)`) рядом с `columnsCurStage`. - `web/src/components/RealtimeTable/Cell/Cell/Cell.jsx` — без новых цветов в палитрах (`constants/columnColors.js` не менялся): - редактируемая ячейка строки INPUT: белый фон, рамка 1px solid #7FA7C9 (2px при наведении через локальный `isHovered` state — pseudo-класс `:hover` недоступен для inline-стилей), курсор text; - нередактируемая ячейка строки INPUT (вычисляемая или вне этапа): фон #F4F5F7, текст #5F6B7A, курсор default; строки ITEM/GROUP/ROOT не тронуты; - занятая другим пользователем: фон не меняется, рамка 2px dashed #F0A030 + значок 🔒 (раньше — серый фон #f5f5f5, визуально похоже на read-only); - порядок спредов сохранён: `isNegative` fontWeight → editable/non-editable → `isLocked` → `isInvalid` (последний, см. выше). - `web/src/components/RealtimeTable/utils/deadlineFormat.js` (новый) — `formatDeadline(closesAt)` → `«до DD.MM HH:mm»` в локальном времени браузера. - `web/src/components/RealtimeTable/TableHead/TableHead.jsx` — `HeaderCell` получает `stageColumnDeadlines`/`userRoleId` (прокинуты из `RealtimeTable.jsx`) и показывает метку `✎ до 31.10 18:00` под заголовком колонки текущего этапа, только исполнителю (не админу). - `web/src/components/RealtimeTable/SettingPanel/CellLegend.jsx` (новый) — иконка «?» с `Tooltip` из двух образцов («можно вводить» / «расчёт или закрыто на этапе»), подключена рядом с плашкой «Контроль ошибок» в `SettingPanel.jsx`, без отдельной панели. - Режим «только текущие колонки» (AURORA-1889) не трогал — использует тот же `columnsCurStage`, его форма не изменилась. ## Тесты `node --test src/components/RealtimeTable/utils/cellEditability.test.js src/components/RealtimeTable/utils/deadlineFormat.test.js` — 18/18 зелёных: - `cellEditability.test.js` (14 тестов) — все примеры из тикета (админ + `data.q1.corrected_plan`/`data.approved.q1`/`data.totals.fact_year`/`data.header.vsp_address` → false; админ + `data.plan.q1` → true; исполнитель вне этапа → false; INPUT+этап → true; ITEM → false) плюс покрытие каждого правила `isComputedColumn` (qN-поля, FORM_3 quarter_actual/total_corr, `*.year`, сателлиты, лист лимитов). - `deadlineFormat.test.js` (4 теста) — формат `«до DD.MM HH:mm»`, паддинг нулями, Date-инстанс напрямую, falsy/invalid → null. Полный `node --test src/components/RealtimeTable/utils/*.test.js` — 62/63 (единственный сторонний фейл, `columnLayoutUtils.test.js`, из-за отсутствующего `node_modules/@tanstack/table-core` в этом worktree — не связан с этим PR, воспроизводится и на базовом коммите без моих изменений). Линт: `biome check` на изменённых файлах не добавляет новых находок к существующему состоянию веток #277/#278/test (сверено diff-ом error/warning-счётчиков до и после изменения каждого файла); единственная находка, которую я внёс сам (nested ternary в новой логике Cell.jsx), исправлена до коммита. Ручная проверка — по коду (живой стенд не поднимал): формы 1/2/4, сателлиты, лист проекта — исполнитель в открытом окне видит белые редактируемые ячейки с синей рамкой и метку срока в заголовке; исполнитель без окна и админ н�� вычисляемых колонках видят серые ячейки без рамки; админ не видит метку срока ввода. ## Rulings - **Текст деадлайна/курсор наведения через React state, не CSS.** У ячеек нет своего stylesheet-класса (`className='cell'` — просто маркер, в проекте нет `.cell` CSS-правил), поэтому `:hover` для утолщения рамки 1px→2px реализован через локальный `isHovered` (`onMouseEnter`/`onMouseLeave`), а не псевдокласс. - **Локальное время текста метки через `Date`, а не `date-fns`.** В проекте уже есть `date-fns`, но соседние утилиты `RealtimeTable/utils` (`headerYearUtils.js`, `web/src/utils/formatDate.js`) форматируют даты через чистый `Date`/`padStart` без доп. зависимостей — для единообразия `deadlineFormat.js` сделан так же. - **Текст пробрасывается как `Map`, а не массив.** Сигнатура `isCellEditable` в тикете использует `stageColumns.has(columnId)`, поэтому `tableColumns.jsx` строит `Set` из `columnsCurStage` (массива) один раз на пересборку колонок; `useRealtimeData.js` для `stageColumnDeadlines` сразу возвращает `Map`, так как `TableHead.jsx` делает точечные `.get(columnId)` по одной колонке на рендер хедер-ячейки, а не проверку принадлежности множеству. - **`data.totals.go_balance_only` (OTCH9F) помечен как computed, хотя в SQL (`_apply_form_cell`) это поле редактируемое.** Тикет явно требует: «scope `approved.*` и `totals.*` — всегда вычисляемые» без исключений — это сознательное упрощение фронтового списка (см. п.2 тикета: «Список зашит в коде... это фронтовый список с тестом»), один известный false positive для одного поля одного сателлита, не исправлял за рамками тикета. - **Текст метки — убрал серый цвет текста `#999999` у занятой ячейки.** Тикет описывает только рамку/бейдж для locked-ячейки («фон ячейки не менять, рамка 2px dashed #F0A030 и значок 🔒»), но старый код также красил текст в серый, что усиливало прежнее сходство с read-only — убрал, оставил обычный brightness-based цвет текста, чтобы отличие было только в рамке/бейдже, как в критерии приёмки («отличается от нередактируемой»).
aep added 4 commits 2026-11-01 12:02:19 +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>
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/280-aurora-1890-cell-editability:feat/280-aurora-1890-cell-editability
git checkout feat/280-aurora-1890-cell-editability
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#281
No description provided.