docs(specs): archive account-ledger-explicit-filters

This commit is contained in:
2026-08-27 14:55:59 +03:00
parent b5df309728
commit 69d4e63a51
6 changed files with 219 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-08-27
@@ -0,0 +1,82 @@
## Context
Страница полного журнала `account-operations` уже фильтруется по периоду и категориям, но фильтры применяются немедленно на каждое изменение контрола. Текущее состояние — два нативных `<input type="date">` и строка чекбоксов для категорий, все в состоянии 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. |
@@ -0,0 +1,51 @@
## Why
На странице «Журнал операций» фильтры применяются немедленно на каждое
изменение контрола, выбор даты сделан нативными `<input type="date">` с
браузерным оформлением, а категории выложены строкой из чекбоксов. Пользователь
ожидает: понятный визуальный вид полей, выпадающий список с мультивыбором для
категорий и явные действия «Применить»/«Сбросить», при которых запрос к
серверу идёт только после подтверждения. Сейчас эти ожидания не выполняются.
## 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 сохраняются.
@@ -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** пагинация выполняется по последнему применённому фильтру, а черновик в контролах сохраняется
@@ -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` и обернуть `<DatesProvider settings={{ locale: "ru" }}>` внутри `<MantineProvider>` в `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) без пустых коммитов.
@@ -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** пагинация выполняется по последнему применённому фильтру, а черновик в контролах сохраняется