25 KiB
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 групп
валют или инструментов. Суммы доступны текстом, не только цветом.
Пассивный доход
┌ Период ──────────────────────────────────────────┐
│ С По │
│ [ДД.ММ.ГГГГ] [ДД.ММ.ГГГГ] │
│ [Применить] [Сбросить] │
└──────────────────────────────────────────────────┘
Получено купонами и дивидендами
[Итого в валюте · Купоны · Дивиденды]
[Остальные валюты отдельными карточками]
Налоговые операции не включены в этот результат.
Сравнения
[Предыдущий период · значения и разницы]
[Годом ранее · значения] или текст недоступности
Выплаты
[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 без преобразования или удаления данных. |