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