diff --git a/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/.openspec.yaml b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/.openspec.yaml new file mode 100644 index 0000000..f845845 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/.openspec.yaml @@ -0,0 +1,2 @@ +schema: wealth-ledger-sdd +created: 2026-08-27 diff --git a/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/design.md b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/design.md new file mode 100644 index 0000000..c96866a --- /dev/null +++ b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/design.md @@ -0,0 +1,82 @@ +## Context + +Страница полного журнала `account-operations` уже фильтруется по периоду и категориям, но фильтры применяются немедленно на каждое изменение контрола. Текущее состояние — два нативных `` и строка чекбоксов для категорий, все в состоянии React. Backend API `/api/v1/brokerage-accounts/{accountId}/operations` уже поддерживает параметры `from`, `to`, `categories`, `cursor`, `limit` и нормализует категории (порядок и дубликаты не создают отдельную cache identity). Кэш страниц Account Ledger (Ledger Page Cache Key) уже определён глоссарием и не меняется. + +## Goals / Non-Goals + +**Goals:** +- Сделать применение фильтров явным: черновик не влияет на запрос до подтверждения. +- Сохранить существующий API-контракт, cursor-пагинацию и серверный дефолт периода. +- Визуальное оформление полей в едином стиле продукта (Mantine-контролы, русская локаль календаря). +- Пагинация продолжает работать по последнему применённому фильтру. + +**Non-Goals:** +- Не менять backend API, параметры запроса или серверный дефолт. +- Не синхронизировать фильтры с URL, не добавлять шаринг по ссылке. +- Не добавлять новые измерения фильтров (только период и категории). +- Не менять отображение записей журнала. + +## Decisions + +### Разделение draft и applied состояния +Черновик хранится в `draftFrom`, `draftTo`, `draftCategories`; запрос `useBrokerageOperations` использует `appliedFrom`, `appliedTo`, `appliedCategories`. Применённое состояние = Ledger Page Cache Key identity (User, Brokerage Account, нормализованные период и категории, cursor, limit). Черновик — только UI. + +**Rationale:** минимальное изменение существующей VM-архитектуры; не требует нового слоя состояния за пределами компонента страницы. **Alternatives:** вынести фильтр в URL-параметры (отвергнуто: нет требования шаринга, усложняет пагинацию), глобальное состояние (отвергнуто: фильтр локален для страницы). + +### Два отдельных DateInput вместо range picker +Два поля `DateInput` («С»/«По») с мин/макс-ограничением вместо одного `DatePickerInput type="range"`. + +**Rationale:** сохраняет существующую раскладку, i18n-ключи `ledger.from`/`ledger.to`, поддерживает частичные фильтры (только «от» или только «по») явно, минимальный diff. Range picker компактнее визуально, но сложнее в partial-состояниях и клавиатурном вводе. + +### MultiSelect для категорий +Mantine `MultiSelect` с `clearable`, placeholder «Выберите категории», данные из `operationCategories`. + +**Rationale:** стандартный паттерн мультивыбора в Mantine, searchable out-of-the-box. **Alternatives:** Popover+Checkbox.Group (отвергнуто: дублирует существующую модель), Select multiple (отвергнуто: менее заметен выбор). + +### DatesProvider с русской локалью +Глобальный `DatesProvider settings={{ locale: "ru" }}` в `App.tsx` + `import "dayjs/locale/ru"`. + +**Rationale:** календарь DateInput показывает русские названия месяцев/дней недели. Без этого — английская локаль dayjs по умолчанию. + +### Зависимости с точными версиями +`@mantine/dates` и `dayjs` объявляются прямыми зависимостями с точными версиями (pin), как остальные пакеты в `package.json`. + +**Rationale:** конвенция репо — exact pins; Knip требует объявлять используемые пакеты. + +## Delivery Map + +| Area | Status | Architectural outcome or N/A reason | +| --- | --- | --- | +| frontend | `affected` | Draft/applied state в VM страницы; DateInput×2 + MultiSelect + «Применить»/«Сбросить» в view; DatesProvider ru; pinned deps; risk-based тесты | +| backend | `N/A` | API-контракт, параметры запроса и серверный дефолт не меняются | +| specs | `affected` | Delta spec добавляет Requirement «User can explicitly apply ledger filters» в `brokerage-account-ledger` | +| docs | `affected` | `docs/features/brokerage-account-ledger.md` обновляет раздел о поведении фильтров | + +### Contract Gate Decision + +| Gate | Status | Reason | +| --- | --- | --- | +| runtime-openapi | `N/A` | Backend API-контракт не меняется; frontend OpenAPI snapshot остаётся прежним | + +## Risks / Trade-offs + +- **Биом-касты**: `value as Date | null` и `as OperationCategory[]` могут триггерить `no-unnecessary-type-assertion` — убрать при флаге линтера, проверить через `npm run verify`. +- **Календарь без dayjs/ru**: без `DatesProvider` или `import "dayjs/locale/ru"` календарь англоязычный — критичный UX-дефект для RU-интерфейса. +- **Тестовое покрытие**: текущий тест покрывает apply+reset пагинации, но не «черновик не шлёт запрос» и «Сбросить очищает»; добавить risk-based тесты. + +## Migration Plan + +Порядок: +1. Specs: delta spec (уже создан). +2. Frontend: deps → DatesProvider → view/vm полировка → тесты → gate verify. +3. Docs: обновить as-built feature doc. +4. Archive: после всех gates. + +Нет миграции данных, нет изменения контракта — односторонняя frontend-замена контролов. + +### Rollback Contract + +| Field | Decision | +| --- | --- | +| Status | `N/A` | +| Reason | Нет необратимых решений: frontend-only change контролов, без API/данных/auth/security. Откат = revert commit. | diff --git a/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/proposal.md b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/proposal.md new file mode 100644 index 0000000..c1bab46 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/proposal.md @@ -0,0 +1,51 @@ +## Why + +На странице «Журнал операций» фильтры применяются немедленно на каждое +изменение контрола, выбор даты сделан нативными `` с +браузерным оформлением, а категории выложены строкой из чекбоксов. Пользователь +ожидает: понятный визуальный вид полей, выпадающий список с мультивыбором для +категорий и явные действия «Применить»/«Сбросить», при которых запрос к +серверу идёт только после подтверждения. Сейчас эти ожидания не выполняются. + +## What Changes + +Намерение: на странице «Журнал операций» заменить нативный выбор периода на +контрол в едином визуальном стиле продукта, собрать категории в выпадающий +список с мультивыбором и сделать применение фильтров явным: черновик в +контролах не меняет видимый список до подтверждения, подтверждение применяет +черновик и сбрасывает пагинацию на первую страницу, сброс фильтров очищает +черновик и немедленно возвращает список к пустому фильтру (сохранив серверный +дефолт периода «с 1 января текущего года»). + +Non-goals: +- Не меняется API-контракт, параметры запроса, cursor-пагинация или + серверный дефолт периода. +- Не вводится URL-синхронизация фильтров, шаринг по ссылке и back/forward + по значениям фильтров. +- Не добавляются новые измерения фильтров (только период и категории из + существующих категорий: `trade`, `income`, `tax`, `fee`, `transfer`, + `other`). +- Не изменяется отображение записей, подпись типа события или направление + денежного движения. + +## Capabilities + +### New Capabilities +- _none_ + +### Modified Capabilities +- `brokerage-account-ledger`: новое requirement «User can explicitly apply + ledger filters» — явное применение черновика периода и категорий, сброс + фильтров и сохранение пагинации по применённому состоянию. Существующие + requirements о серверной фильтрации, cache identity и компактном + представлении записей остаются без изменений. + +## Impact + +- Пользователи страницы полного журнала брокерского счёта — визуальное + оформление фильтров и явное применение. +- Frontend-страница `account-operations` — новые контролы и draft/applied + семантика. +- Backend и API-контракт: не затрагиваются. +- Кэш страниц Account Ledger: identity не меняется — нормализация + категорий, нормализованный период, cursor и limit сохраняются. diff --git a/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/specs/brokerage-account-ledger/spec.md b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/specs/brokerage-account-ledger/spec.md new file mode 100644 index 0000000..fbf8a72 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/specs/brokerage-account-ledger/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: User can explicitly apply ledger filters +Система MUST позволять User формировать черновик периода и категорий на странице полного журнала и применять его явным действием. Пока черновик изменён, но не применён, видимый список записей и его курсорная пагинация MUST оставаться основанными на последнем применённом состоянии. Явное применение MUST коммитить черновик в применимое состояние и сбрасывать курсорную пагинацию на первую страницу. Сброс фильтров MUST очищать черновик, немедленно применять пустой фильтр (период по умолчанию и все категории) и сбрасывать пагинацию. + +#### Scenario: Черновик изменён, но не применён +- **WHEN** User изменил период или категории в контролах фильтров, но не нажал «Применить» +- **THEN** видимый список записей остаётся основанным на последнем применённом фильтре, а новый запрос к серверу не выполняется + +#### Scenario: Применение черновика +- **WHEN** User нажал «Применить» на фильтре журнала с изменённым черновиком +- **THEN** черновик коммитится в применимое состояние, курсорная пагинация сбрасывается на первую страницу и выполняется новый запрос с применённым периодом и категориями + +#### Scenario: Сброс фильтров +- **WHEN** User нажал «Сбросить» на фильтре журнала +- **THEN** черновик очищается, немедленно применяется пустой фильтр (период по умолчанию и все категории) и курсорная пагинация сбрасывается на первую страницу + +#### Scenario: Пагинация по применённому фильтру +- **WHEN** User переходит к следующей или предыдущей странице журнала при изменённом, но не применённом черновике +- **THEN** пагинация выполняется по последнему применённому фильтру, а черновик в контролах сохраняется diff --git a/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/tasks.md b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/tasks.md new file mode 100644 index 0000000..2f87f97 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-account-ledger-explicit-filters/tasks.md @@ -0,0 +1,45 @@ +## 1. Frontend implementation + +- [x] 1.1 [frontend] Запинить `@mantine/dates` 9.5.1 и `dayjs` 1.11.23 в `package.json` без caret-префиксов; синхронизировать `package-lock.json`. + - **Covers:** `N/A` — техническая необходимость: репо использует exact pins, Knip требует объявленные зависимости. + - **Verification:** `cd frontend && npm ci` без изменений lockfile. + +- [x] 1.2 [frontend] Добавить `import "dayjs/locale/ru"` в `src/main.tsx` и обернуть `` внутри `` в `src/app/ui/App.tsx`. + - **Covers:** `N/A` — техническая необходимость: без этого календарь DateInput показывает английскую локаль. + - **Verification:** `npm run typecheck` проходит; ручная проверка попапа календаря показывает русские названия месяцев. + +- [x] 1.3 [frontend] Добавить i18n-ключ `ledger.categoriesPlaceholder: "Выберите категории"` в `src/shared/i18n/locales/ru.ts` и заменить placeholder у MultiSelect в `AccountOperationsPageLoaded.view.tsx`. + - **Covers:** `brokerage-account-ledger` — `User can explicitly apply ledger filters` (UI-полировка мультивыбора категорий). + - **Verification:** MultiSelect placeholder показывает «Выберите категории» вместо дубликата label. + +- [x] 1.4 [frontend] Проверить и убрать ненужные касты (`value as Date | null`, `as OperationCategory[]`) в `AccountOperationsPageLoaded.view.tsx`, если Biome/TS-линтер флагают `no-unnecessary-type-assertion`. + - **Covers:** `N/A` — техническая необходимость: gate `npm run lint` должен проходить. + - **Verification:** `npm run lint` без ошибок type assertion. + +- [x] 1.5 [frontend] Добавить risk-based тесты в `AccountOperationsPage.test.tsx`: (a) изменение черновика НЕ вызывает новый запрос до «Применить»; (b) «Сбросить» очищает категории и период и перезапрашивает без параметров. + - **Covers:** `brokerage-account-ledger` — `User can explicitly apply ledger filters` (сценарии «Черновик изменён, но не применён» и «Сброс фильтров»). + - **Verification:** `npm run test:run` — оба новых теста проходят. + +## 2. Docs update + +- [x] 2.1 [docs] Обновить `docs/features/brokerage-account-ledger.md` пункт 32: описать staged draft/applied семантику фильтров, явные «Применить»/«Сбросить», пагинацию по применённому фильтру. + - **Covers:** `brokerage-account-ledger` — `User can explicitly apply ledger filters` (as-built документация). + - **Verification:** `cd docs && npm run verify` проходит. + +## 3. Gates and archive + +- [x] 3.1 [frontend] Запустить canonical gate: `cd frontend && npm ci && npm run verify`. + - **Covers:** `N/A` — финальная проверка frontend-реализации. + - **Verification:** typecheck + lint + coverage-ratchet + knip + deps + build проходят. + +- [x] 3.2 [specs] Запустить `openspec validate account-ledger-explicit-filters --type change --strict --store wealth-ledger-specs` и `openspec validate --specs --strict --store wealth-ledger-specs`. + - **Covers:** `N/A` — structural validation change и main specs. + - **Verification:** Обе команды exit 0. + +- [x] 3.3 [workspace] Semantic verification реализации против утверждённых Proposal, Delta Specs, Design и Tasks; independent read-only review. + - **Covers:** `N/A` — оркестрация финальной проверки перед archive. + - **Verification:** Reviewer-confirmation без Critical/Important findings. + +- [x] 3.4 [specs] Отметить tasks выполненными и archive change через `openspec archive`. + - **Covers:** `N/A` — закрытие change lifecycle. + - **Verification:** `openspec status` показывает change archived; commits в affected repos (specs, frontend, docs) без пустых коммитов. diff --git a/openspec/specs/brokerage-account-ledger/spec.md b/openspec/specs/brokerage-account-ledger/spec.md index c837256..1cf98d8 100644 --- a/openspec/specs/brokerage-account-ledger/spec.md +++ b/openspec/specs/brokerage-account-ledger/spec.md @@ -108,3 +108,22 @@ #### Scenario: Просроченная страница не возвращается - **WHEN** cached страница журнала старше 30 секунд - **THEN** система не возвращает её как свежий результат и получает актуальную страницу из upstream + +### Requirement: User can explicitly apply ledger filters +Система MUST позволять User формировать черновик периода и категорий на странице полного журнала и применять его явным действием. Пока черновик изменён, но не применён, видимый список записей и его курсорная пагинация MUST оставаться основанными на последнем применённом состоянии. Явное применение MUST коммитить черновик в применимое состояние и сбрасывать курсорную пагинацию на первую страницу. Сброс фильтров MUST очищать черновик, немедленно применять пустой фильтр (период по умолчанию и все категории) и сбрасывать пагинацию. + +#### Scenario: Черновик изменён, но не применён +- **WHEN** User изменил период или категории в контролах фильтров, но не нажал «Применить» +- **THEN** видимый список записей остаётся основанным на последнем применённом фильтре, а новый запрос к серверу не выполняется + +#### Scenario: Применение черновика +- **WHEN** User нажал «Применить» на фильтре журнала с изменённым черновиком +- **THEN** черновик коммитится в применимое состояние, курсорная пагинация сбрасывается на первую страницу и выполняется новый запрос с применённым периодом и категориями + +#### Scenario: Сброс фильтров +- **WHEN** User нажал «Сбросить» на фильтре журнала +- **THEN** черновик очищается, немедленно применяется пустой фильтр (период по умолчанию и все категории) и курсорная пагинация сбрасывается на первую страницу + +#### Scenario: Пагинация по применённому фильтру +- **WHEN** User переходит к следующей или предыдущей странице журнала при изменённом, но не применённом черновике +- **THEN** пагинация выполняется по последнему применённому фильтру, а черновик в контролах сохраняется