Files
specs/openspec/changes/simplify-income-page/design.md
T

269 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
Изменение ограничено presentation и page orchestration маршрута
`/accounts/:accountId/income`. Текущий `AccountIncomePage` выбирает состояния
через page VM; VM владеет периодом из URL, формой и запросами, а section views
отображают переданные данные. Верхняя форма использует native date inputs;
история событий уже использует установленный `@mantine/dates` DateInput.
Ответ `income-analytics` уже содержит totals, comparisons, instrument breakdown
и все включённые entries. VM соединяет entries с инструментами по непрозрачному
`instrumentKey`, проверяет валюту и receipt count. Денежные значения и порядок
инструментов приходят с backend. Дополнительная подписка income VM на `/events`
обслуживает только удаляемый прогноз. Trend view использует отдельную derived
модель из buckets того же ответа.
Ограничения: FSD `app → pages → entities → shared`, чистые section views,
существующие Mantine theme и i18n, без новой инфраструктуры или зависимостей.
Даты API и URL остаются календарными `YYYY-MM-DD` в Europe/Moscow;
формат отображения поля не меняет их смысл.
## Goals / Non-Goals
**Goals:** использовать существующий календарный контрол, сохранить явную
границу draft/applied period, отобразить полный состав дохода в отдельных
карточках выплат и удалить принадлежащие странице зависимости исчезающих разделов.
Сохранить проверки целостности membership и текущие денежные вычисления.
**Non-Goals:** изменение API, серверных buckets, денежных агрегатов, определения
Instrument, общих Ledger rows, обзора счёта, самостоятельной страницы будущих
событий, общей темы или навигации. Пагинация карточек, виртуализация,
дополнительные фильтры и новый каталог компонентов не вводятся.
## Decisions
### 1. Календарные поля адаптируются на границе page form
Используется Mantine DateInput с `valueFormat="DD.MM.YYYY"` и существующим
русским DatesProvider. Форма остаётся под управлением react-hook-form и Zod в
page VM; form-context/controlled-field adapter подключает DateInput к ней.
Общий date-range widget не создаётся: income требует обязательного периода,
запрета будущих дат и явных ошибок, тогда как Ledger допускает другие фильтры.
Импорта между page slices нет.
VM различает видимый черновик и применённые ISO-даты. Ручной ввод, в том числе
пустой или недопустимый текст, должен попасть в проверяемый черновик. Адаптер
учитывает raw input, календарный выбор и blur; автоматический откат текста
DateInput к последнему допустимому значению на blur отключается (`fixOnBlur=false`).
Парсинг допускает только существующую календарную дату формата поля, без
нормализации вроде 31 февраля в март. Успешный календарный выбор заменяет
черновик поля; валидированный submit переводит обе даты в ISO и обновляет URL.
Последнее внутреннее допустимое значение DateInput не заменяет ошибочный ввод.
Календарь ограничивает начало допустимым окончанием и сегодняшней московской
датой, окончание — допустимым началом и сегодняшней датой. Невалидное соседнее
поле не передаётся в min/max. Ограничения календаря помогают выбору, но Zod
остаётся проверкой обязательности, календарной корректности, будущих дат и
порядка. Для отсутствующей, некорректной или будущей даты ошибка принадлежит
самому полю; при обратном диапазоне — полю «По».
Адаптация календарной даты использует компоненты года, месяца и дня, как в
Ledger, без UTC-преобразования через `toISOString()` пользовательского Date.
«Сегодня» и default period определяются существующим московским правилом VM,
а не часовым поясом устройства. URL остаётся источником применённого периода;
его текущая нормализация, открытие и browser navigation сохраняются.
Сброс очищает ошибки и восстанавливает оба поля даже тогда, когда текущий URL
уже содержит default period, а пользователь изменил только черновик.
Альтернативы: native date inputs не обеспечивают согласованный контрол;
единый range picker меняет привычное взаимодействие двух полей; копирование
всего Ledger filter state переносит неподходящие income семантики. Выбран
существующий DateInput с локальной адаптацией формы.
### 2. Одна карточка соответствует одной выплате
`IncomePayments` отображает один семантический список: каждый Ledger Entry
передаётся существующему `LedgerEntryRow`. Нет внешней Paper-карточки Instrument,
заголовков валютных групп, instrument subtotal/total и receipt count на UI.
Как в списке позиций портфеля, карточки идут общей колонкой; импортов между
page slices и нового row renderer нет. LedgerEntryRow сохраняет дату, описание,
доступную identity, signed Money, характеристику выплаты и fallback изображения.
VM сначала проверяет opaque instrument keys, их непустоту и уникальность,
принадлежность каждой entry к известному instrumentKey, совпадение валюты и
receipt count. Валюта инструмента должна присутствовать среди totalsByCurrency.
Повреждённый ответ идёт целиком в error/retry, без частично заполненного списка.
Клиент не восстанавливает identity формулой и не пересчитывает денежные суммы.
После проверки VM отдаёт плоский список в исходном порядке `analytics.entries`.
В ключ строки входят opaque instrumentKey, operation ID, occurrence внутри
этой группы и logo source. Одинаковые IDs не схлопываются, в том числе между
инструментами. Валюта видна в каждой Money, а общие summary/comparisons по-прежнему
разделены по валютам. Серверный порядок не заменяется клиентской сортировкой.
### 3. Удаление секций завершает их локальное владение
Income page прекращает вызывать `useBrokerageEvents` и готовить forecast model.
Удаляются принадлежащие только этой странице forecast/trend views, derived
trend model и неиспользуемые стили/строки этих секций. Shared event entity,
её запрос и самостоятельная страница будущих событий сохраняются.
Backend продолжает возвращать buckets по прежнему контракту; frontend больше
не строит из них presentation для income page. Это позволяет ограничить
изменение потребителем без миграции transport types и соседних consumers.
Удаление полей API ради размера ответа — отдельное изменение контракта,
не необходимое для этого решения.
### 4. Границы состояний и доказательства остаются у страницы
Top-level loading/error и loaded composition остаются в существующей
container/VM/view структуре. Skeleton отражает новые регионы. Пустой результат
использует StateView; ошибка запроса или membership — существующий error/retry.
При фоновой загрузке того же периода валидный Client Cached Snapshot сохраняется
с неблокирующим progress, а после ошибки — с локальным предупреждением и retry
согласно frontend UI conventions. При отсутствии данных нового query используется loading, без старого результата.
Отдельной подписи применённого периода под формой нет; draft/applied boundary
сохраняется и видимые суммы меняются только после применения.
Проверка опирается на существующие page tests и MSW и защищает период и состав
денежных данных: draft/apply/reset/URL, недопустимый ручной ввод, московскую границу дня,
несколько валют, отрицательные и повторяющиеся выплаты, повреждённый membership
и retry. Удаляемые chart/forecast expectations заменяются наблюдаемым отсутствием
секций и запроса events от income page. Сохраняемые денежные/membership assertions
не удаляются вместе с Accordion. Адаптивность и keyboard flow оцениваются по
пакету ниже; новая test/browser инфраструктура для этого не требуется.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — income page: календарные фильтры,
плоский список выплат, последовательность секций и loading representation.
Общая оболочка, обзор счёта и самостоятельные «Будущие события» вне scope.
**Package revision:** `simplify-income-page-ui-r2`.
Пакет полностью inline; дополнительных attachments, external-only макетов
и immutable reuse inputs нет. Revision r2 отражает уточнение владельца:
убрать подпись периода, заголовки валют и внешние карточки Instrument,
переименовать раздел в «Выплаты», показывать отдельную карточку каждой выплаты.
**Screens, states and interactions:**
Loaded: существующая ширина content до 960 px, Stack gap `lg`, текущая светлая
Mantine theme. h1 страницы; h2 итогов, сравнений и «Выплаты». Нет h3/h4 групп
валют или инструментов. Суммы доступны текстом, не только цветом.
```text
Пассивный доход
┌ Период ──────────────────────────────────────────┐
│ С По │
│ [ДД.ММ.ГГГГ] [ДД.ММ.ГГГГ] │
│ [Применить] [Сбросить] │
└──────────────────────────────────────────────────┘
Получено купонами и дивидендами
[Итого в валюте · Купоны · Дивиденды]
[Остальные валюты отдельными карточками]
Налоговые операции не включены в этот результат.
Сравнения
[Предыдущий период · значения и разницы]
[Годом ранее · значения] или текст недоступности
Выплаты
[Ledger Entry: описание, дата, identity, signed Money]
[Следующая Ledger Entry, отдельная карточка]
```
Форма — одна Paper с border, radius `md`, padding `md`, видимым «Период».
Подписанные DateInput «С» и «По» имеют placeholder `ДД.ММ.ГГГГ`, штатный
календарь по focus/click, `valueFormat="DD.MM.YYYY"`, strict raw validation и
`fixOnBlur=false`. Действия отдельной строкой: «Применить» filled, «Сбросить»
default. Под формой нет повторяющей даты подписи. Неприменённый черновик не
меняет аналитику; ограничения и московское правило описаны в Decision 1.
«Выплаты» — один `ul`, одна LedgerEntryRow-карточка на `li`, gap `sm`.
Не добавляются внешние instrument cards, currency headings, instrument totals,
subtotals/count, chevron, раскрытие или внутренние scroll-панели. Метаданные и
logo fallback принадлежат существующему LedgerEntryRow. Все строки сразу видимы;
денежные значения не обрезаются, при нехватке ширины переносятся.
| Состояние | Представление |
| --- | --- |
| Initial loading / новый период без cached data | Busy skeleton повторяет heading, форму, summary/comparisons и отдельные строки выплат; shapes без фокуса, shell сохраняется. |
| Loaded | Итоги, сравнения и плоский список всех выплат; валюты указаны в Money, summary/comparisons не смешивают их. |
| Изменён draft | Поля отражают ввод; суммы, сравнения и выплаты сохраняют последний применённый результат. |
| Invalid draft | Ошибка под соответствующим полем, фокус на первом ошибочном поле, исходный текст и URL сохраняются, запроса нет. |
| Apply | Click или Enter проверяет raw draft; только успех меняет URL/query, повторный submit во время обработки блокируется. |
| Reset | Немедленно восстанавливает обе даты текущего московского месяца, очищает ошибки и синхронизирует URL даже при изменённом только draft. |
| Empty | Фильтры, «Купоны и дивиденды не найдены» / «За выбранный период подходящих исполненных выплат нет.», налоговое пояснение, сравнения; «Выплаты» без фиктивных карточек. |
| Error без результата / повреждённый membership | StateView «Не удалось рассчитать пассивный доход», пояснение, «Повторить» текущий applied period; частичные данные скрыты. |
| Фоновое обновление того же периода | Cached snapshot и draft сохраняются; progress не блокирует; при ошибке предупреждение с retry. |
| Нет year comparison | «Сравнение с годом ранее недоступно.», без вымышленных нулей. |
| Отрицательные суммы / неполная metadata | Исходный знак, доступная identity и существующий logo fallback; запись не исключается. |
Во всех состояниях отсутствуют trend, buckets UI, forecast и его состояния.
Keyboard order: «С» → «По» → «Применить» → «Сбросить»; Escape закрывает календарь.
Штатные доступные controls Mantine и focus indicator сохранены. Ошибки связаны
с inputs; суммы и неинтерактивные строки не получают tabIndex. Все продуктовые
подписи принадлежат русскому i18n.
**Responsive behavior:**
| Viewport | Форма | Выплаты |
| --- | --- | --- |
| От `48em` | Два равных поля, отдельная строка кнопок по содержимому. | Одна колонка LedgerEntryRow; identity и сумма используют существующий layout. |
| `36em`–`48em` | Поля рядом, кнопки переносятся при необходимости. | Одна колонка, текст и суммы переносятся без обрезки. |
| Менее `36em` | Поля и кнопки вертикально на всю ширину, touch target не менее 44 px. | Существующий mobile row: сумма под identity. |
На 320 px и при увеличенном масштабе страница не требует горизонтального scroll;
popover остаётся в viewport. Высота определяется данными, прокрутка общая,
пагинация, виртуализация и ограничения количества строк не вводятся.
**Reused source and differences:** immutable visual reuse `N/A`.
DateInput истории, LedgerEntryRow, StateView, LoadingSkeleton и theme —
implementation references. Поля income обязательны и запрещают будущие даты;
список выплат плоский. Код страницы портфеля не копируется между slices.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Calendar form adapter, плоский список карточек выплат, composition/states и относящиеся к ним проверки; удаление только income-owned trend/forecast consumer code. |
| backend | `N/A` | Текущий income-analytics уже возвращает totals/comparisons, отсортированные instruments и полные entries с instrumentKey. Endpoint, buckets, денежные расчёты, events и хранение не меняются. |
| specs | `affected` | Canonical planning и последующая синхронизация согласованных delta requirements в capability brokerage-account-income-analytics по lifecycle. |
| docs | `affected` | После реализации актуализируется as-built описание пассивного дохода: календарные фильтры, отдельные карточки выплат, отсутствие подписи периода и trend/forecast UI и использование прежнего analytics response. |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `N/A` | API contract не меняется ни на одной стороне; frontend потребляет прежний типизированный ответ и прекращает отдельный вызов events только со страницы дохода. Snapshot/types не требуют регенерации. |
## Risks / Trade-offs
- Открытый список увеличивает высоту и число одновременно отрисованных строк.
Это ожидаемая цена согласованного полного состава выплат. Используется один
проход группировки в VM и обычный document flow; лимиты, скрытие строк или
виртуализация не подменяют согласованное поведение.
- Raw input DateInput, permissive calendar parsing и timezone conversion могут
незаметно подменить период. Локальный form adapter и строгая проверка видимого
черновика защищают эту границу; одних minDate/maxDate недостаточно.
- Замена Accordion может потерять повторяющиеся выплаты или ошибочные membership
guards при упрощении VM. Сохраняются исходный порядок, occurrence identity и
существующий отказ от повреждённого результата.
- Удаление forecast UI должно удалить и его query subscription; shared events
остаётся нужен самостоятельной странице и не удаляется вместе с consumer.
- В текущем main Purpose осталось упоминание отдельного ближайшего прогноза.
Согласованная delta явно удаляет требование его показа на income page.
Это известная устаревающая формулировка Purpose, не разрешение вернуть UI:
native sync сохраняет existing Purpose и незатронутые requirements. В этом
change Purpose не переписывается; его последующая редактура требует отдельного
разрешённого изменения. Упоминания buckets в сохраняемых серверных инвариантах
совместимы с неизменным API и не требуют их визуализации.
## Migration Plan
Поставка выполняется без серверной или data migration. Сначала обновляется
согласованная frontend composition и её адаптация формы в рамках существующих
границ; после удаления consumers очищаются их локальные неиспользуемые части.
Сохраняемые сценарии периода и membership проходят проверку вместе с новой
компоновкой. Затем docs отражает фактически поставленный поток. Синхронизация
main specs и archive остаются последующими canonical completion mechanics.
Предварительный backend rollout, изменение environment и установка новых
зависимостей не нужны. API остаётся совместимым со старым и новым frontend.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `N/A` |
| Reason | Обратимое read-only изменение frontend UI; auth/security, public API, persistent data и денежные вычисления не меняются. Возврат прежней frontend версии восстанавливает прежнее отображение на том же backend без преобразования или удаления данных. |