Миграция 0053: v3.budget_line.source_sheet (NULL | AHR_LIMIT | AHR_RENT |
AHR_UTILITY | AHR_SECURITY) — явный признак источника строки вместо
сопоставления по совпадению имени с LEAF limit_template. Backfill
существующих строк: сателлиты — из v3.form2_ahr_satellite_line_source,
лимитные — минимальный id в группе (форма, статья, имя LEAF), остальные
совпавшие по имени становятся ручными (RAISE NOTICE со списком).
_ensure_form2_ahr_limit_sync_line теперь ищет строку по source_sheet='AHR_LIMIT'
(а не по имени) — тот же класс бага, что чинил #265/AURORA-1909 для сателлитов
аренды/коммуналки/охраны, теперь для лимитов. is_form2_ahr_satellite_line,
del_budget_line (guard на удаление) и get_form2_ahr_satellite_delta переведены
на признак источника. Новая v3._form2_expense_item_has_limit выносит правило
«у статьи есть лимит» в отдельную функцию.
v_form2_sheet_jsonb: header.source ('AHR_LIMIT'/'AHR_RENT'/'AHR_UTILITY'/
'AHR_SECURITY'/'MANUAL'), header.is_over_limit для ручных строк на статьях
с лимитом; plan.limit_year/over_limit_year (только на листе AHR, свёрнуты по
дереву статей через mv_expense_item_tree.desc_ids) — plan.year не меняется,
остаётся суммой всех строк.
Фронт: AHR.js — колонки «В пределах лимита, год»/«Сверх лимита, год» после
«План, год» (нередактируемые — нет случая в v3._apply_form_cell); Cell.jsx —
чип «сверх лимита» в ячейке наименования по header.is_over_limit, без заливки
строки. export_mappings.py — те же два поля в экспорт АХР.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Пока AURORA-1896 не перенесёт формулы на сервер, временно исправляем
потерю данных в EditCell:
- resolveEditorState/isSameCellValue (formulaUtils.js) — чистые функции:
формула подставляется в редактор только если её результат совпадает
с текущим значением ячейки, иначе она устарела (stale) и удаляется
из localStorage.
- EditCell: открытие и закрытие ячейки без правки больше не шлёт
cell_updated (сверка с initialEditorValue при выходе из редактора;
для обычных значений — дополнительно через isSameCellValue). Если
введённая формула не вычисляется — toast и редактор остаётся открытым,
на сервер ничего не уходит.
- tableColumns.jsx: ключ формулы строится от getRowId(cell.row) (line_id
из данных), а не от cell.row.id (позиционный индекс-путь в дереве,
переезжающий при вставке/удалении строк). Префикс ключа — formula_v2_.
- RealtimeTable: при монтировании once-очистка устаревших ключей
formula_* без суффикса _v2 — они привязаны к позиции строки и стали
недостоверны.
Тесты: web/src/components/RealtimeTable/utils/formulaUtils.test.js,
node --test — 9/9 новых + 5/5 ранее существующих в utils/ зелёные.
Closes#264.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Добавлен столбец «СПОД» между «Декабрь» и «Итог IV кв.» на листах
AHR_RENT/AHR_UTILITY/AHR_SECURITY формы 2, с переносом значения в
budget_line_quarter.actual_spod синхронизируемой строки АХР (Support)
и, через уже существующую логику, в итог 4 кв. и года АХР/Смет/сводов.
- rent_detail/utility_detail/security_detail: колонка actual_spod
(ALTER TABLE + ORM).
- v3.v_form2_view: fact_q4.spod и fact_q4.total с учётом spod в ветках
AHR_RENT/AHR_UTILITY/AHR_SECURITY.
- v3._apply_form_cell: fact_q4.spod разрешён как обычное поле месяца.
- v3._upsert_form2_ahr_detail_sync_values: DROP+CREATE с новым
параметром p_actual_spod (DROP обязателен из-за неоднозначности
перегрузки с DEFAULT).
- v3.sync_form2_ahr_satellite_lines/line: actual_spod передаётся из
rd/ud/sd в апсерт.
- v3.v_svod2_go: строка аренды также считается заполненной по
actual_spod.
- Data-миграция: 'fact_q4.spod' добавлен в column_keys этапов, где уже
есть 'fact_q4.dec' (phase_template/form_phase), идемпотентно.
- Тело CREATE OR REPLACE для v_form2_view/sync_form2_ahr_satellite_*
собрано вручную по миграциям 0033/0034 + regexp-патчам из 0037
(pg_get_functiondef на живой БД недоступен в этой среде — см.
Rulings в PR).
Тесты: node --test на новых accessorKey в конфигах фронта + unit-тест
на 0049/0050 (v_svod2_go теперь засчитывает actual_spod для аренды).
Интеграционные тесты добавлены, но не выполнены — нет доступа к
Postgres в этой рабочей среде (см. Rulings в PR).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- FormPhaseRepository.get_list и Form3PhaseRepository.get_list: новый
необязательный параметр role для фильтрации по v3.role.code.
- FormPhaseService.get_list и Form3PhaseService.get_list: для не-админа
передают в репозиторий role по коду роли пользователя (единая функция
role_code_for_user в form_phase_service.py), для ADMIN фильтр не нужен.
- GET /stages/form/{form_id} и GET /stages/project/{project_id}: сначала
проверяют доступ к форме/проекту (BudgetFormService.get /
ProjectService.get_instance), при отсутствии доступа — 404 вместо
AttributeError → 500.
- StagesTable.jsx: подпись роли этапа показывается только админу.
Closes#250.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Выгрузка Excel листов АХР/КВ/Операц/Структура форм 1 и 4 расходилась с экраном
по нескольким независимым причинам:
- FORM_4: _patch_form4_regular_columns() вызывался дважды — второй вызов
стирал только что добавленные actual_rsbu_spod_q1..q3, поэтому в выгрузке
были только «Факт 4 кв + СПОД», без «Факт 1/2/3 кв». Убрал второй вызов
и больше не удаляю эти ключи в самом патче.
- _patch_form4_actual_rsbu_spod_columns: SQL отдаёт actual_rsbu_spod только
для 4 кв.; для 1-3 кв. ключ теперь q{n}.actual_quarter (как на экране),
не q{n}.actual_rsbu_spod (всегда пустой).
- Базовая раскладка FORM1_DEPTH_COLUMNS (отражается и в FORM_4, т.к. она из неё
строится): убрал collegial.empty и группы "shift" (ключ sum, SQL не отдаёт);
в q2/q3 добавил comment_correct (adj_comment) сразу после reserve_correct —
раньше его не было вовсе; в q4 то же поле вынес из rev_correct в отдельный
comment_correct на то же место (было в начале квартала вместе с комментарием
корректировки плана). group "approved.plan" переименована из скопированного
"Резерв Финансового Комитета" в "План {year}"; q4 "quarter_correct" — из
ошибочного "Факт РСБУ 3 кв" в "Факт РСБУ 4 кв {year_short}".
- render_column_years: добавлен плейсхолдер year_plus_2 (year+2); contract_summary
получил future_y2 "Платежи в {year_plus_2} году" (раньше его не было, и FORM_4
patch всё равно дублировал его с неверным {next_year}). На экране «Операц»
формы 1 такой колонки нет — для неё новая FORM1_OPER_DEPTH_COLUMNS без future_y2.
- FORM4_OPER: proc_plan/proc_method после переименования уезжали в конец блока
ckk (pop+переприсвоение добавляет ключ в конец словаря) — вернул порядок
экрана через _reorder_children.
- web/.../FORM_1/{AHR,CAP,OPER}.js: в q4 узел "Комментарий" (adj_comment, GG)
шёл после "Скорректированный план" (corrected_plan, GH) — переставил перед,
как в 1-3 кв.
Issue: #253 (AURORA-1905, эпик AURORA-1876, Высокий приоритет)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Листы Операц/АХР/КВ формы «Смета РФ» (FORM_2) выгружались раскладкой формы 1
(FORM1_DEPTH_COLUMNS, 180 колонок формы 1) — из 163 колонок экрана совпадали
по ключу только 83, 97 колонок выгрузки были всегда пустыми (блоки формы 1,
которых нет в данных формы 2), заголовки групп — от формы 1. Также на экране
был лишний столбец «Адрес ВСП», которого нет в исходной форме РФ, и ВСП
выгружался бы числовым id вместо номера, который виден на экране.
- FORM2_DEPTH_COLUMNS/FORM2_COLORS (export_mappings.py) — сгенерированы из
web/src/.../FORM_2/{OPER,AHR,CAP}.js (колонки идентичны у всех трёх листов,
проверено программно; различаются только цвета). 162 листа дерева —
байт-в-байт тот же порядок, что на экране, минус header.vsp_address.
Каждая группа — чисто визуальная обёртка ("key": None), каждый лист несёт
полный путь данных как "key" (например "booking.y2027.q1"), поэтому порядок
вложенности групп для Excel-заголовков не завязан на реальный путь к данным.
- Form2DepthRowNormalizer (export_normalizers.py) — в отличие от
Form1DepthRowNormalizer, не поднимает header.* на верхний уровень: FORM_2
ожидает их как header.section_code, header.vsp_id и т.д.
- _create_xlsxwriter_sheet_writers: отдельная ветка для FormTypeEnum.FORM_2
(раньше FORM_2 попадала в общий else вместе с FORM_1). Сателлиты АХР
(AHR_RENT/UTILITY/SECURITY, AURORA-1904) и STRUCTURE не трогал.
- VSPRepository.get_reg_numbers_by_ids + ExportService._resolve_form2_vsp_numbers:
один запрос резолвит все встретившиеся header.vsp_id (включая закрытые ВСП)
в registration_number перед записью в Excel, как уже показывает экран.
- web/.../FORM_2/{OPER,AHR,CAP}.js: убран столбец «Адрес ВСП» (E1) и его цвет —
по умолчанию, т.к. его нет в исходной форме РФ (см. Rulings в PR). Убрана
мёртвая ссылка на него в Stages/constants.js.
Issue: #252 (AURORA-1902, эпик AURORA-1876, Высокий приоритет)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
На листе «Смета» формы «Смета РФ» итоговые колонки каждой группы
(план/утверждённые/факт «Итого: Поддержка» и IV квартал скорректированной
сметы) были оторваны от своих групп и собраны в отдельную группу
«В тыс. рублей» справа — хотя в шаблоне идут сразу после своего квартала.
- Перенёс H/N/T (data.*.support.year) в конец соответствующих групп план/
утверждённые/факт, X (data.corrected.support.q4) — в конец группы
скорректированной сметы после III квартала.
- Удалил опустевшую группу «В тыс. рублей»; accessorKey групп
(v_tys_rubley_*) нигде больше не использовались (grep по web/src).
columnLetter и leaf accessorKey (data.*) не менял — видимые значения
те же, меняется только расположение колонок.
- Тест читает FORM_2/SMETA.js как текст (модуль не грузится напрямую —
импортирует '../columnColors' без расширения) и проверяет порядок
accessorKey листьев и отсутствие заголовка «В тыс. рублей».
Issue: #251 (AURORA-1901, эпик AURORA-1876, Высокий приоритет)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Удаление строки в формах 1/2/4 не проверяло владельца: любой исполнитель
мог удалить чужую строку мгновенно, без подтверждения.
- v3.del_budget_line (миграция 0050): не-admin может удалить только
budget_line, где created_by совпадает с current_user_id() (проверка
и для обычных листов, и для AHR_RENT/UTILITY/SECURITY — через
created_by связанной budget_line). Нарушение -> role_not_allowed:
(403), по существующей конвенции PG_403_ERRORS.
- RealtimeTable: кнопка «Удалить строку» теперь открывает
ConfirmDeleteRowModal, реальное удаление — только после подтверждения.
- Интеграционные тесты: новый исполнитель isp2 (fixture.sql) в том же
org_unit, что и budget_form id=1 — проверяют 403 при чужой строке,
200 при своей и при удалении администратором.
Issue: #249 (AURORA-1936, эпик AURORA-1876, Высокий приоритет)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Access-токен жил 60 минут без обновления: фронт не слал response-interceptor
на 401, refresh-токен лежал в localStorage неиспользуемым, WebSocket-сокеты
(форма, статусы выгрузки) не переподключались после закрытия с auth-кодом,
а после редиректа на /login пользователь терял исходную страницу.
- tokenRefresh.js: чистые getTokenExp/msUntilRefresh/createSingleFlight (TDD).
- auth.js: refreshTokens — обновление токена вне интерсепторов api (без
рекурсии), централизованно чистит токены и шлёт auth:expired при неудаче.
- client.js: response-interceptor повторяет запрос после обновления токена.
- AuthProvider: при старте и по 'auth:expired'/'auth:refreshed' не выкидывает
из системы, пока жив refresh; проактивный таймер обновления + visibilitychange.
- Routes/LoginPage: PrivateRoute сохраняет исходный location, вход возвращает
туда же вместо /tasks; убрана мёртвая ветка loading.
- useWebSocket/exports: переподключение после 1008/4401 с одной попыткой
refreshTokens() перед тем как сдаться.
Issue: #248 (AURORA-1877, эпик AURORA-1876, Критичный приоритет)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Хук useWheelScrollStep перехватывает щелчок колеса мыши над таблицей и
прокручивает вдвое больше (~100 px браузера -> 200 px). Тачпад, Ctrl и
Shift + колесо, горизонтальная прокрутка и поля ввода не меняются.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Чистые функции getWheelScrollDelta и canScrollBy для шага прокрутки
колесом мыши в таблице форм (AURORA-1944), с тестами node:test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ячейки строк с data-svod2-placeholder рисуют MUI Skeleton wave высотой 20 с детерминированной шириной.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Конфликты в Svod2Page.jsx (импорты, content) и svod2Styles.test.js разрешены
объединением: серверная пагинация 1843 плюс grid-no-grow, pinning и портал заголовка 1842.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MRT при виртуализации колонок рисует каждую строку заголовка по индексам
листовых колонок, из-за чего двухуровневые группы разъезжались. Штатный thead
отключён (enableTableHead: false), тот же MRT_TableHead без columnVirtualizer
вставляется порталом в <table>: все заголовки сразу, стили и встроенные фильтры
MRT сохраняются. Тело по-прежнему виртуализировано по строкам и колонкам.
Стили: order -1 для портального thead, flexShrink 0 у td/th (распорки
виртуализации сжимались и сдвигали тело на 2-8 px), скрыта подложка ::before
закреплённых ячеек (перекрывала чередование фона).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Копируются только строки на пути к затронутой, остальные сохраняют идентичность;
без изменений возвращается тот же data. Отсутствующие в row.data ключи теперь
вставляются (нужно для AURORA-1844/1848). Тесты с паритетом к прежней реализации.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Высота строки и ячейки задана явно в rem, виртуализатор не читает offsetHeight.
Мёртвая muiTableRowProps удалена. Overscan строк свода 2: 8 -> 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Модалки в DictVspModals (forwardRef), таблица в memo со стабильными колбэками,
enableRowVirtualization, size/minSize/maxSize колонок выверены под grid-раскладку.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AURORA-1835: Cell в scaleColumns создавал Intl.NumberFormat на каждую ячейку.
Форматирование вынесено в formatSummaryAmount (summaryFormat.js) с модульным форматтером.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Открытие модалки больше не перерисовывает RealtimeTable: состояние живёт
в memo-компоненте SheetModals, наружу открывается через ref.open(kind).
Проверки выделенной строки остаются в таблице. Редьюсер вынесен в
utils/sheetModalsState.js с node-тестом.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>