Files

25 KiB
Raw Permalink Blame History

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 без преобразования или удаления данных.