spec: record approved income page simplification
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
## 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 без преобразования или удаления данных. |
|
||||
Reference in New Issue
Block a user