# Broker Accounts Overview Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Превратить `/broker` в информативный обзор одного–трёх брокерских счетов с общей сводкой, независимой загрузкой карточек и устойчивыми состояниями ошибок. **Architecture:** `BrokerAccountsPage` получает список счетов и передаёт его в новый hook на основе TanStack Query `useQueries`; query key совпадает с `useBrokerPortfolio`, поэтому детальная страница переиспользует кеш. Чистый модуль агрегации группирует денежные значения по валютам и рассчитывает единственный новый финансовый показатель по формуле из spec. UI разбит на общую сводку, карточку счёта и компактную полосу распределения; backend и OpenAPI не меняются согласно ADR-012. **Tech Stack:** React 18, TypeScript, TanStack Query v5, React Router v6, Vitest, Testing Library, CSS custom properties. --- ## Карта файлов - Create `apps/frontend/src/pages/broker/brokerAccountsOverview.ts` — чистые типы, агрегация валютных сумм и форматирование дат/денег/процентов. - Create `apps/frontend/src/pages/broker/brokerAccountsOverview.test.ts` — unit-тесты финансовой агрегации и edge cases. - Create `apps/frontend/src/hooks/useBrokerAccountPortfolios.ts` — параллельные portfolio queries с общими query keys. - Create `apps/frontend/src/hooks/useBrokerAccountPortfolios.test.tsx` — проверка независимых query-состояний и кеша. - Create `apps/frontend/src/pages/broker/BrokerAccountsSummary.tsx` — общая сводка и частичное состояние. - Create `apps/frontend/src/pages/broker/BrokerAccountCard.tsx` — успешная карточка, skeleton и локальная ошибка с retry. - Create `apps/frontend/src/pages/broker/BrokerAllocationBar.tsx` — доступная горизонтальная полоса распределения. - Create `apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx` — page/component tests. - Modify `apps/frontend/src/pages/broker/BrokerAccountsPage.tsx` — orchestration новой страницы. - Modify `apps/frontend/src/pages/broker/BrokerPages.test.tsx` — удалить старый поверхностный тест списка счетов. - Modify `apps/frontend/src/styles.css` — визуальная система overview, focus/hover, skeleton и responsive rules. - Modify `docs/features/broker-accounts-overview/tasks.md` — отмечать выполненные задачи. - Modify `docs/roadmap.md` — отметить фичу реализованной только после всех проверок. ### Task 1: Чистая модель агрегации **Files:** - Create: `apps/frontend/src/pages/broker/brokerAccountsOverview.test.ts` - Create: `apps/frontend/src/pages/broker/brokerAccountsOverview.ts` - [ ] **Step 1: Написать падающие unit-тесты** Покрыть одним набором тестов: ```ts import { describe, expect, it } from 'vitest'; import type { BrokerPortfolio } from '../../api/responses'; import { aggregateBrokerAccounts } from './brokerAccountsOverview'; function portfolio( id: string, currency: string, total: number, daily: number | null, cash: number, ): BrokerPortfolio { return { account: { id, type: 'brokerage', name: id, status: 'ACCOUNT_STATUS_OPEN', openedAt: '2022-06-16T00:00:00.000Z', accessLevel: null, }, positionCounts: { shares: 1, bonds: 1, etf: 0, other: 0 }, totals: { shares: { currency, units: '0', nano: 0, value: total * 0.5 }, bonds: { currency, units: '0', nano: 0, value: total * 0.3 }, etf: null, currencies: { currency, units: '0', nano: 0, value: total * 0.2 }, futures: null, options: null, structuredProducts: null, dfa: null, portfolio: { currency, units: '0', nano: 0, value: total }, }, yields: { expectedPercent: 10, daily: daily === null ? null : { currency, units: '0', nano: 0, value: daily }, dailyPercent: null, }, cash: [{ currency, units: '0', nano: 0, value: cash }], blockedCash: [], asOf: '2026-06-19T10:00:00.000Z', }; } describe('aggregateBrokerAccounts', () => { it('sums comparable portfolios and uses the specified daily percent formula', () => { const result = aggregateBrokerAccounts([ portfolio('a', 'RUB', 1_100, 100, 200), portfolio('b', 'RUB', 2_200, 200, 300), ]); expect(result.portfolios).toEqual([ expect.objectContaining({ currency: 'RUB', total: 3_300, daily: 300, dailyPercent: 10, allocation: { shares: 1_650, bonds: 990, etf: 0, cash: 660, other: 0 }, }), ]); expect(result.cash).toEqual([{ currency: 'RUB', value: 500 }]); }); it('keeps different currencies separate', () => { const result = aggregateBrokerAccounts([ portfolio('rub', 'RUB', 1_100, 100, 200), portfolio('usd', 'USD', 550, 50, 25), ]); expect(result.portfolios.map(({ currency, total }) => ({ currency, total }))).toEqual([ { currency: 'RUB', total: 1_100 }, { currency: 'USD', total: 550 }, ]); }); it('does not expose a daily percent when one account lacks daily data', () => { const result = aggregateBrokerAccounts([ portfolio('a', 'RUB', 1_100, 100, 200), portfolio('b', 'RUB', 2_000, null, 300), ]); expect(result.portfolios[0]).toMatchObject({ daily: null, dailyPercent: null }); }); }); ``` - [ ] **Step 2: Запустить unit-тест и подтвердить RED** Run: `npx vitest run src/pages/broker/brokerAccountsOverview.test.ts -w apps/frontend` Expected: FAIL с ошибкой импорта `./brokerAccountsOverview`. - [ ] **Step 3: Реализовать минимальную чистую модель** Создать публичные типы `BrokerAccountsAggregate`, `BrokerCurrencyPortfolioSummary`, `BrokerCurrencyCashSummary` и функцию: ```ts export function aggregateBrokerAccounts(portfolios: BrokerPortfolio[]): BrokerAccountsAggregate; ``` Правила реализации: - пропускать портфель без `totals.portfolio` или без currency; - группировать `totals.portfolio`, `yields.daily` и классы активов по currency портфеля; - считать `other` как `max(0, portfolio - shares - bonds - etf - currencies)`; - группировать `portfolio.cash` независимо по валюте; - если хотя бы в одном портфеле валютной группы нет сопоставимого `yields.daily`, возвращать для группы `daily: null` и `dailyPercent: null`; - иначе считать `dailyPercent = daily / (total - daily) * 100`, только если знаменатель положителен; - сортировать валютные группы по первому появлению во входном массиве, не по алфавиту. - [ ] **Step 4: Запустить unit-тест и подтвердить GREEN** Run: `npx vitest run src/pages/broker/brokerAccountsOverview.test.ts -w apps/frontend` Expected: PASS, 3 tests. - [ ] **Step 5: Добавить edge cases** Добавить тесты для неположительной стоимости начала дня, отрицательного residual `other`, пустого массива, отсутствующего `totals.portfolio` и cash в нескольких валютах. Реализация должна возвращать конечные числа и никогда не смешивать валюты. - [ ] **Step 6: Запустить unit-тесты и commit** Run: `npx vitest run src/pages/broker/brokerAccountsOverview.test.ts -w apps/frontend` Expected: PASS. ```bash git add apps/frontend/src/pages/broker/brokerAccountsOverview.ts apps/frontend/src/pages/broker/brokerAccountsOverview.test.ts git commit -m "feat: aggregate broker account summaries" ``` ### Task 2: Независимые portfolio queries **Files:** - Create: `apps/frontend/src/hooks/useBrokerAccountPortfolios.ts` - Create: `apps/frontend/src/hooks/useBrokerAccountPortfolios.test.tsx` - [ ] **Step 1: Написать падающий hook-тест** Mock `getBrokerPortfolio`, отрендерить hook с двумя счетами и проверить, что вызываются `acc-1` и `acc-2`, а результат сохраняет соответствие `account → query` независимо от порядка завершения Promise. Второй тест должен создать `QueryClient`, заранее положить портфель в key `['broker', 'portfolio', 'acc-1']` и подтвердить, что hook использует то же кешированное значение. - [ ] **Step 2: Запустить hook-тест и подтвердить RED** Run: `npx vitest run src/hooks/useBrokerAccountPortfolios.test.tsx -w apps/frontend` Expected: FAIL с ошибкой импорта `useBrokerAccountPortfolios`. - [ ] **Step 3: Реализовать hook через `useQueries`** ```ts import { useQueries } from '@tanstack/react-query'; import { getBrokerPortfolio } from '../api/broker'; import type { BrokerAccount, BrokerPortfolio } from '../api/responses'; export function useBrokerAccountPortfolios(accounts: BrokerAccount[]) { const queries = useQueries({ queries: accounts.map((account) => ({ queryKey: ['broker', 'portfolio', account.id], queryFn: async (): Promise => (await getBrokerPortfolio(account.id)).data, staleTime: 60_000, retry: 2, refetchOnWindowFocus: false, })), }); return accounts.map((account, index) => ({ account, query: queries[index] })); } ``` - [ ] **Step 4: Запустить hook-тест и подтвердить GREEN** Run: `npx vitest run src/hooks/useBrokerAccountPortfolios.test.tsx -w apps/frontend` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add apps/frontend/src/hooks/useBrokerAccountPortfolios.ts apps/frontend/src/hooks/useBrokerAccountPortfolios.test.tsx git commit -m "feat: load broker account portfolios in parallel" ``` ### Task 3: Компоненты и состояния страницы **Files:** - Create: `apps/frontend/src/pages/broker/BrokerAccountsSummary.tsx` - Create: `apps/frontend/src/pages/broker/BrokerAllocationBar.tsx` - Create: `apps/frontend/src/pages/broker/BrokerAccountCard.tsx` - Create: `apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx` - Modify: `apps/frontend/src/pages/broker/BrokerAccountsPage.tsx` - Modify: `apps/frontend/src/pages/broker/BrokerPages.test.tsx` - [ ] **Step 1: Написать page/component tests до реализации** Mock `useBrokerAccounts` и `useBrokerAccountPortfolios`. Проверить отдельными тестами: - heading, агрегированную сумму, дневной результат и две карточки; - подписи `Брокерский счёт` и `ИИС`, форматированную дату открытия и отсутствие ID/raw enum; - href всей карточки `/broker/:encodedAccountId`; - skeleton при загрузке списка; - пустое состояние при `accounts: []`; - частичную сводку `Доступно по 1 из 2 счетов`; - локальный alert и кнопку `Повторить` для ошибочного query; - вызов `query.refetch()` по кнопке retry; - раздельное отображение RUB и USD без суммирования. Для денежных assertions использовать regexp с обычным и non-breaking space, например: ```ts expect(screen.getByText(/3[\s\u00a0]?300[\s\u00a0]?₽/)).toBeInTheDocument(); ``` - [ ] **Step 2: Запустить page-тест и подтвердить RED** Run: `npx vitest run src/pages/broker/BrokerAccountsPage.test.tsx -w apps/frontend` Expected: FAIL, потому что новые компоненты и состояния отсутствуют. - [ ] **Step 3: Реализовать `BrokerAllocationBar`** Компонент получает `BrokerAllocationItem[]`, строит сегменты с inline `width: percent%`, добавляет `role="img"`, осмысленный `aria-label` со всеми долями и текстовую легенду. Нулевые сегменты не рендерятся; цвет не является единственным способом различить классы. - [ ] **Step 4: Реализовать `BrokerAccountsSummary`** Компонент получает aggregate, `loadedCount` и `totalCount`. Он показывает: - статус `Совокупный капитал · N счетов` либо `Доступно по N из M счетов`; - по одному блоку стоимости/дневного результата на валюту; - свободные деньги отдельным списком валют; - allocation bar только когда доступна одна portfolio currency; - placeholder `—`, если ни один портфель ещё не загружен. - [ ] **Step 5: Реализовать `BrokerAccountCard`** Компонент получает `account` и query result. Три ветки должны иметь стабильную геометрию: ```tsx if (query.isPending) return ; if (query.error || !query.data) { return query.refetch()} />; } return ; ``` Успешная карточка — одна `Link` на `/broker/${encodeURIComponent(account.id)}`. Внутри показать название, тип, дату открытия, стоимость, `yields.daily`, `dailyPercent`, `expectedPercent` и allocation bar из существующего `buildBrokerAllocation(portfolio).sectors`. - [ ] **Step 6: Переписать orchestration `BrokerAccountsPage`** Страница должна: 1. вызвать `useBrokerAccounts()`; 2. передать `accounts ?? []` в `useBrokerAccountPortfolios` без условного вызова hooks; 3. построить aggregate только из `query.data` успешных записей; 4. показать page error только при ошибке списка; 5. показать отдельное empty state для пустого списка; 6. отрендерить summary и вертикальный список карточек. - [ ] **Step 7: Удалить устаревший тест из `BrokerPages.test.tsx`** Удалить test case `renders broker and IIS accounts` и неиспользуемые imports `accountHook` и `BrokerAccountsPage`; покрытие новой страницы живёт в `BrokerAccountsPage.test.tsx`. - [ ] **Step 8: Запустить component tests и подтвердить GREEN** Run: `npx vitest run src/pages/broker/BrokerAccountsPage.test.tsx src/pages/broker/BrokerPages.test.tsx -w apps/frontend` Expected: PASS. - [ ] **Step 9: Commit** ```bash git add apps/frontend/src/pages/broker/BrokerAccountsPage.tsx apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx apps/frontend/src/pages/broker/BrokerAccountsSummary.tsx apps/frontend/src/pages/broker/BrokerAccountCard.tsx apps/frontend/src/pages/broker/BrokerAllocationBar.tsx apps/frontend/src/pages/broker/BrokerPages.test.tsx git commit -m "feat: add informative broker accounts overview" ``` ### Task 4: Визуальная система и responsive QA **Files:** - Modify: `apps/frontend/src/styles.css` - Modify: `apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx` - [ ] **Step 1: Добавить семантические CSS-классы** Добавить блоки `.broker-accounts`, `__header`, `__summary`, `__summary-metrics`, `__list`, `.broker-account-card`, `__topline`, `__value`, `__metrics`, `.broker-allocation-bar`, `__track`, `__legend`, `__error` и `__empty`. Визуальное направление: - тёплый нейтральный фон страницы и глубокий зелёный summary без градиента; - serif-акцент только для крупных денежных значений, основной текст наследует текущую гарнитуру; - тонкие границы и мягкая тень карточек, без вложенных «карточек в карточке»; - один заметный hover карточки: небольшой подъём и усиление тени; - `:focus-visible` с контрастным outline; - positive/negative цвета всегда сопровождаются знаком и текстом; - `prefers-reduced-motion: reduce` отключает transform/transition. - [ ] **Step 2: Добавить responsive rules** При `max-width: 720px` summary metrics и card metrics переходят в одну колонку, легенда allocation переносится, денежные значения уменьшаются через `clamp()`, а карточка и кнопка retry сохраняют минимальную интерактивную высоту 44px. Горизонтальный overflow на `.broker-accounts` запрещён. - [ ] **Step 3: Запустить frontend проверки** Run: `npm run test -w apps/frontend` Expected: PASS. Run: `npm run lint -w apps/frontend` Expected: PASS без warnings. Run: `npm run build -w apps/frontend` Expected: успешный TypeScript и Vite build. - [ ] **Step 4: Проверить страницу в локальном браузере** Запустить frontend и backend по README. Проверить `/broker` при ширинах 1280px и 390px: - нет горизонтального overflow; - summary визуально доминирует, но карточки остаются читаемыми; - карточки и retry доступны с клавиатуры; - loading не меняет геометрию страницы; - partial error не скрывает успешные счета; - названия, крупные суммы и allocation legend не перекрываются. - [ ] **Step 5: Commit** ```bash git add apps/frontend/src/styles.css apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx git commit -m "style: polish broker accounts overview" ``` ### Task 5: Финальная документация и Definition of Done **Files:** - Modify: `docs/features/broker-accounts-overview/tasks.md` - Modify: `docs/roadmap.md` - [ ] **Step 1: Отметить выполненные tasks** Поставить `[x]` только после соответствующих commit и проверок. Не менять ADR-012: решение уже Accepted и реализация ему соответствует. - [ ] **Step 2: Обновить roadmap** Изменить строку фичи на: ```md - [x] [Информативный обзор брокерских счетов](features/broker-accounts-overview/spec.md) — реализовано. ``` - [ ] **Step 3: Выполнить полный verification gate** Run: `npm run test -w apps/frontend && npm run lint -w apps/frontend && npm run build -w apps/frontend` Expected: все команды завершаются с exit code 0. Run: `npm run build -w apps/docs` Expected: Docusaurus build succeeds. - [ ] **Step 4: Провести code review** Использовать `superpowers:requesting-code-review`. Исправить замечания только после технической проверки; при изменении поведения сначала синхронизировать spec/plan. - [ ] **Step 5: Финальный commit документации** ```bash git add docs/features/broker-accounts-overview/tasks.md docs/roadmap.md git commit -m "docs: complete broker accounts overview" ``` ## Definition of Done - Все Acceptance Criteria из `spec.md` имеют component или unit coverage. - Frontend tests, lint и build проходят. - Docusaurus build проходит. - Desktop и mobile UI проверены в браузере. - `tasks.md` и roadmap соответствуют реализации. - Нет изменений backend, OpenAPI или `apps/frontend/src/api/types.ts`. - Нет незавершённых маркеров или отложенных требований.