# Broker Dashboard Redesign 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:** Rebuild `/broker/:accountId` overview as a light-theme investment dashboard using current broker portfolio, events, operations, analytics, and allocation data. **Architecture:** Keep the route and FSD boundaries unchanged. Add focused dashboard widgets under `widgets/broker-dashboard` and keep domain data access in existing `entities/*` hooks. Extend `@moex-vibe/design-system` only for generic presentation gaps; do not move broker-specific logic into DS. **Tech Stack:** React 18, TanStack Router, TanStack Query, MUI via `@moex-vibe/design-system`, Vitest, Testing Library. --- ## File Structure - Modify: `apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx` — replace vertical overview composition with dashboard shell. - Create: `apps/frontend/src/widgets/broker-dashboard/index.ts` — public API for dashboard widgets. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` — top-level dashboard composition. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardHero.tsx` — KPI hero using portfolio and analytics data. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx` — local card pattern if DS `Card`/`Surface` remains too generic. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx` — compact events table for overview. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx` — compact income table from operations. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx` — analytics summary card. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAllocationCard.tsx` — allocation card wrapping existing allocation chart logic. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardSkeleton.tsx` — dashboard-shaped loading skeleton. - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.ts` — pure helpers for filtering and summarising dividend/coupon operations. - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts` — date preset, filter validation, operation type mapping, and pagination helpers. - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFormatters.ts` — local presentation helpers for KPI fallback and event status labels. - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx` — composition tests. - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.test.ts` — income helper unit tests. - Modify if needed: `packages/design-system/src/components/Surface/Surface.tsx` — allow `sx` passthrough for generic surface customization. - Modify if needed: `packages/design-system/src/components/Chip/Chip.tsx` — support selected/active visual state without broker-specific concepts. - Modify if DS changed: corresponding DS tests and stories. - Modify: `docs/features/broker-dashboard-redesign/tasks.md` — mark implementation tasks as completed while working. ## Data Sources - Portfolio: `useBrokerAccountContext().portfolio` from `BrokerAccountLayout`. - Events: `useBrokerEvents(accountId, { from, to, types })` from `entities/broker-event`. - Operations: `useBrokerOperations(accountId, { from, to, operationTypes, cursor, limit: 10 })` from `entities/broker-operation`. - Analytics: `useBrokerAnalytics(accountId)` from `entities/broker-analytics`. - Allocation: `buildBrokerAllocation(portfolio)` from `entities/broker-position` or existing `BrokerAllocationChart` internals. ## Tasks ### Task 1: Income Helper **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.ts` - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.test.ts` - [ ] **Step 1: Write failing tests for income filtering and totals** Create `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.test.ts`: ```ts import type { BrokerOperation } from '@/shared/api' import { getDashboardIncomeRows, isDashboardIncomeOperation, sumDashboardIncome } from './dashboardIncome' function operation(type: string, value: number | null): BrokerOperation { return { cursor: null, accountId: 'acc-1', id: type, parentOperationId: null, date: '2026-06-01T00:00:00.000Z', type, category: 'income', description: null, name: 'Apple Inc.', state: 'OPERATION_STATE_EXECUTED', instrumentUid: null, figi: null, ticker: 'AAPL', classCode: null, instrumentType: 'share', payment: value === null ? null : { currency: 'RUB', units: String(Math.trunc(value)), nano: 0, value }, price: null, commission: null, yield: null, accruedInt: null, quantity: null, quantityDone: null, } } describe('dashboardIncome', () => { it('detects dividend and coupon operation types', () => { expect(isDashboardIncomeOperation(operation('OPERATION_TYPE_DIVIDEND', 10))).toBe(true) expect(isDashboardIncomeOperation(operation('OPERATION_TYPE_DIV_EXT', 10))).toBe(true) expect(isDashboardIncomeOperation(operation('OPERATION_TYPE_COUPON', 10))).toBe(true) expect(isDashboardIncomeOperation(operation('OPERATION_TYPE_BUY', -10))).toBe(false) }) it('returns only displayable income rows with payments', () => { const rows = getDashboardIncomeRows([ operation('OPERATION_TYPE_DIVIDEND', 10), operation('OPERATION_TYPE_BUY', -10), operation('OPERATION_TYPE_COUPON', null), ]) expect(rows).toHaveLength(1) expect(rows[0].typeLabel).toBe('Дивиденд') expect(rows[0].amount.value).toBe(10) }) it('sums displayed income rows by currency', () => { const rows = getDashboardIncomeRows([ operation('OPERATION_TYPE_DIVIDEND', 10), operation('OPERATION_TYPE_COUPON', 2.5), ]) expect(sumDashboardIncome(rows)).toEqual({ currency: 'RUB', value: 12.5 }) }) }) ``` - [ ] **Step 2: Run the failing test** Run: `rtk npm run test:frontend -- --run src/widgets/broker-dashboard/lib/dashboardIncome.test.ts` Expected: FAIL because `dashboardIncome.ts` does not exist. - [ ] **Step 3: Implement income helpers** Create `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.ts`: ```ts import type { BrokerMoney, BrokerOperation } from '@/shared/api' const INCOME_TYPES = new Set([ 'OPERATION_TYPE_DIVIDEND', 'OPERATION_TYPE_DIV_EXT', 'OPERATION_TYPE_COUPON', ]) export type DashboardIncomeRow = { id: string date: string | null instrument: string typeLabel: 'Дивиденд' | 'Купон' amount: BrokerMoney } export function isDashboardIncomeOperation(operation: BrokerOperation): boolean { return INCOME_TYPES.has(operation.type) && operation.payment !== null } function typeLabel(type: string): DashboardIncomeRow['typeLabel'] { return type === 'OPERATION_TYPE_COUPON' ? 'Купон' : 'Дивиденд' } export function getDashboardIncomeRows(operations: BrokerOperation[]): DashboardIncomeRow[] { return operations.filter(isDashboardIncomeOperation).map((operation) => ({ id: String(operation.id ?? operation.cursor ?? `${operation.type}-${operation.date}`), date: typeof operation.date === 'string' ? operation.date : null, instrument: typeof operation.ticker === 'string' ? operation.ticker : typeof operation.name === 'string' ? operation.name : typeof operation.description === 'string' ? operation.description : '—', typeLabel: typeLabel(operation.type), amount: operation.payment!, })) } export function sumDashboardIncome(rows: DashboardIncomeRow[]): { currency: string; value: number } | null { if (rows.length === 0) return null const currency = rows[0].amount.currency const value = rows.reduce((sum, row) => sum + row.amount.value, 0) return { currency, value } } ``` - [ ] **Step 4: Verify helper tests pass** Run: `rtk npm run test:frontend -- --run src/widgets/broker-dashboard/lib/dashboardIncome.test.ts` Expected: PASS. ### Task 2: Dashboard Formatting Helpers **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts` - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFormatters.ts` - [ ] **Step 1: Add filter helpers** Create `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts`: ```ts import dayjs from 'dayjs' export const DASHBOARD_EVENT_TYPES = ['dividend', 'coupon', 'maturity', 'offer'] as const export const DASHBOARD_INCOME_TYPES = ['dividend', 'coupon'] as const export type DashboardEventType = (typeof DASHBOARD_EVENT_TYPES)[number] export type DashboardIncomeType = (typeof DASHBOARD_INCOME_TYPES)[number] export type DashboardDatePreset = '7d' | '30d' | '90d' | '1y' | 'all' export type DashboardFilterState = { from: string to: string types: T[] } export function defaultEventsFilters(): DashboardFilterState { const now = dayjs() return { from: now.subtract(7, 'day').format('YYYY-MM-DD'), to: now.add(7, 'day').format('YYYY-MM-DD'), types: [...DASHBOARD_EVENT_TYPES], } } export function defaultIncomeFilters(): DashboardFilterState { const now = dayjs() return { from: now.startOf('year').format('YYYY-MM-DD'), to: now.format('YYYY-MM-DD'), types: [...DASHBOARD_INCOME_TYPES], } } export function applyDatePreset( filters: DashboardFilterState, preset: DashboardDatePreset, ): DashboardFilterState { const now = dayjs() if (preset === 'all') return { ...filters, from: '', to: now.format('YYYY-MM-DD') } const amount = preset === '1y' ? 1 : Number.parseInt(preset, 10) const unit = preset === '1y' ? 'year' : 'day' return { ...filters, from: now.subtract(amount, unit).format('YYYY-MM-DD'), to: now.format('YYYY-MM-DD') } } export function validateDashboardFilters(filters: DashboardFilterState): string { if (filters.types.length === 0) return 'Выберите хотя бы один тип' if (filters.from && filters.to && dayjs(filters.to).isBefore(dayjs(filters.from))) { return 'Дата окончания не может быть раньше даты начала' } return '' } export function incomeTypesToOperationTypes(types: DashboardIncomeType[]): string { const operationTypes = new Set() if (types.includes('dividend')) { operationTypes.add('OPERATION_TYPE_DIVIDEND') operationTypes.add('OPERATION_TYPE_DIV_EXT') } if (types.includes('coupon')) operationTypes.add('OPERATION_TYPE_COUPON') return [...operationTypes].join(',') } ``` **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFormatters.ts` - [ ] **Step 2: Add event and KPI presentation helpers** Create `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFormatters.ts`: ```ts import type { BrokerEventItem } from '@/shared/api' export function dashboardValue(value: string | null | undefined): string { return value && value.trim().length > 0 ? value : '—' } export function eventTypeLabel(type: BrokerEventItem['type']): string { switch (type) { case 'dividend': return 'Дивиденд' case 'coupon': return 'Купон' case 'maturity': return 'Погашение' case 'offer': return 'Оферта' } } export function eventStatusLabel(event: BrokerEventItem): string { if (event.source === 'actual') return 'Поступило' if (event.type === 'offer') return 'Оферта' return 'Прогноз' } ``` - [ ] **Step 3: Use helpers from dashboard components in later tasks** Expected: no command yet; helpers are compiled by frontend tests in later tasks. ### Task 3: Dashboard Card Pattern **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx` - [ ] **Step 1: Create a local card component** Create `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx`: ```tsx import { Heading } from '@moex-vibe/design-system' import { Box, type SxProps, type Theme } from '@mui/material' import type { ReactNode } from 'react' type BrokerDashboardCardProps = { title: string action?: ReactNode filters?: ReactNode children: ReactNode sx?: SxProps } export function BrokerDashboardCard({ title, action, filters, children, sx }: BrokerDashboardCardProps) { return ( {title} {action} {filters && {filters}} {children} ) } ``` - [ ] **Step 2: Prefer local pattern over DS changes unless repeated needs emerge** Expected: no DS files changed in this task. ### Task 4: Dashboard Hero **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardHero.tsx` - [ ] **Step 1: Implement hero KPI block** Create `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardHero.tsx`: ```tsx import { Metric, Text } from '@moex-vibe/design-system' import { Box } from '@mui/material' import type { BrokerAnalytics, BrokerPortfolio } from '@/shared/api' import { formatBrokerMoney, formatBrokerPercent } from '@/shared/lib/formatters' type BrokerDashboardHeroProps = { portfolio: BrokerPortfolio analytics: BrokerAnalytics | undefined } function percentValue(value: unknown): string { return typeof value === 'number' ? formatBrokerPercent(value) : '—' } export function BrokerDashboardHero({ portfolio, analytics }: BrokerDashboardHeroProps) { const accountName = portfolio.account.name || 'Брокерский счёт' const totalReceived = analytics ? `${analytics.totalReceived.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} ${analytics.currency}` : '—' const returnPercent = analytics?.totalReturnPercent ?? portfolio.yields.expectedPercent return ( Инвестиционный дашборд {accountName} ) } ``` - [ ] **Step 2: Run type-aware frontend tests later through the composition test** Expected: no standalone command for this component. ### Task 5: Events Card **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx` - [ ] **Step 1: Implement compact events card** The dashboard composition owns applied filters and local pagination state; the card renders filter controls and calls callbacks supplied by `BrokerDashboard`: - draft filters: event types, `from`, `to`; - applied filters: stored in `BrokerDashboard` and passed to `useBrokerEvents`; - local page index over `data.items`, with page size 10; - `Показать` applies draft filters and resets local page to 1; - `Сбросить` restores `defaultEventsFilters()` and resets local page to 1; - invalid filters show validation text and disable `Показать`. Create `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx`: ```tsx import { Chip, Text } from '@moex-vibe/design-system' import { Box } from '@mui/material' import { Link } from '@tanstack/react-router' import type { BrokerEventItem, BrokerEventsData } from '@/shared/api' import { formatBrokerCurrencyValue, formatBrokerDate } from '@/shared/lib/formatters' import { eventStatusLabel, eventTypeLabel } from '../lib/dashboardFormatters' import { BrokerDashboardCard } from './BrokerDashboardCard' type BrokerDashboardEventsCardProps = { accountId: string data: BrokerEventsData | undefined isLoading: boolean isError: boolean page: number onPreviousPage: () => void onNextPage: () => void canGoBack: boolean canGoForward: boolean } function eventAmount(event: BrokerEventItem): string { const amount = event.source === 'actual' ? event.actualAmount : event.estimatedAmount if (amount === null || amount === undefined) return '—' const prefix = event.source === 'actual' ? '+' : '~' return `${prefix}${formatBrokerCurrencyValue(event.currency ?? 'RUB', amount)}` } export function BrokerDashboardEventsCard({ accountId, data, isLoading, isError, page, onPreviousPage, onNextPage, canGoBack, canGoForward }: BrokerDashboardEventsCardProps) { const events = data?.items ?? [] return ( Все события} > {isError ? ( Не удалось загрузить события ) : isLoading ? ( Загрузка событий… ) : events.length === 0 ? ( В ближайшем периоде событий нет ) : ( {events.map((event) => ( {formatBrokerDate(event.eventDate)} {event.ticker ?? event.name ?? '—'} {eventTypeLabel(event.type)} {eventAmount(event)} {eventStatusLabel(event)} ))} {page} )} ) } ``` - [ ] **Step 2: Verify later through dashboard composition test** Expected: card handles loading, error, empty and paginated states without throwing. During implementation, use DS `Button` instead of raw ` {pageNumber} )} ) } ``` - [ ] **Step 2: Confirm operations endpoint suffices** Run the app locally and inspect `/broker/:accountId`; if recent operations do not contain enough income rows, document this limitation in the PR notes rather than adding backend API in this feature. - [ ] **Step 3: Replace raw pagination buttons with DS Button during implementation** Expected: final implementation uses `Button` from `@moex-vibe/design-system` for pagination and apply/reset actions. ### Task 7: Analytics and Allocation Cards **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx` - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAllocationCard.tsx` - [ ] **Step 1: Implement analytics card** Create `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx`: ```tsx import { Metric, Text } from '@moex-vibe/design-system' import { Box } from '@mui/material' import type { BrokerAnalytics } from '@/shared/api' import { BrokerDashboardCard } from './BrokerDashboardCard' function amount(value: number, currency: string): string { return `${value.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}` } export function BrokerDashboardAnalyticsCard({ data, isLoading, isError }: { data: BrokerAnalytics | undefined; isLoading: boolean; isError: boolean }) { return ( {isError ? ( Не удалось загрузить аналитику ) : isLoading ? ( Загрузка аналитики… ) : !data ? ( Нет данных для аналитики ) : ( )} ) } ``` - [ ] **Step 2: Implement allocation card** Create `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAllocationCard.tsx`: ```tsx import { Text } from '@moex-vibe/design-system' import { Box } from '@mui/material' import type { BrokerPortfolio } from '@/shared/api' import { formatBrokerMoney } from '@/shared/lib/formatters' import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart' import { BrokerDashboardCard } from './BrokerDashboardCard' export function BrokerDashboardAllocationCard({ portfolio }: { portfolio: BrokerPortfolio }) { return ( {formatBrokerMoney(portfolio.totals.portfolio)} } > ) } ``` - [ ] **Step 3: Run frontend tests after correcting imports/types** Run: `rtk npm run test:frontend -- --run src/widgets/broker-dashboard` Expected: PASS after dashboard tests are added in Task 9. ### Task 8: Dashboard Composition **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardSkeleton.tsx` - Create: `apps/frontend/src/widgets/broker-dashboard/index.ts` - Modify: `apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx` - [ ] **Step 1: Implement dashboard skeleton** Create `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardSkeleton.tsx`: ```tsx import { Skeleton } from '@moex-vibe/design-system' import { Box } from '@mui/material' export function BrokerDashboardSkeleton() { return ( ) } ``` - [ ] **Step 2: Implement dashboard composition** Create `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx`: ```tsx import { Box } from '@mui/material' import { useState } from 'react' import { useBrokerAnalytics } from '@/entities/broker-analytics' import { useBrokerEvents } from '@/entities/broker-event' import { useBrokerOperations } from '@/entities/broker-operation' import type { BrokerPortfolio } from '@/shared/api' import { useCursorPagination } from '@/shared/lib/useCursorPagination' import { defaultEventsFilters, defaultIncomeFilters, incomeTypesToOperationTypes, } from '../lib/dashboardFilters' import { BrokerDashboardAllocationCard } from './BrokerDashboardAllocationCard' import { BrokerDashboardAnalyticsCard } from './BrokerDashboardAnalyticsCard' import { BrokerDashboardEventsCard } from './BrokerDashboardEventsCard' import { BrokerDashboardHero } from './BrokerDashboardHero' import { BrokerDashboardIncomeCard } from './BrokerDashboardIncomeCard' export function BrokerDashboard({ accountId, portfolio }: { accountId: string; portfolio: BrokerPortfolio }) { const [eventFilters, setEventFilters] = useState(defaultEventsFilters) const [eventPage, setEventPage] = useState(1) const [incomeFilters, setIncomeFilters] = useState(defaultIncomeFilters) const incomePagination = useCursorPagination() const analytics = useBrokerAnalytics(accountId) const events = useBrokerEvents(accountId, { from: eventFilters.from, to: eventFilters.to, types: eventFilters.types.join(','), }) const eventItems = events.data?.items ?? [] const eventPageSize = 10 const eventPageItems = eventItems.slice((eventPage - 1) * eventPageSize, eventPage * eventPageSize) const operations = useBrokerOperations(accountId, { from: incomeFilters.from, to: incomeFilters.to, operationTypes: incomeTypesToOperationTypes(incomeFilters.types), cursor: incomePagination.cursor, limit: 10, }) return ( 1} canGoForward={eventPage * eventPageSize < eventItems.length} onPreviousPage={() => setEventPage((page) => Math.max(1, page - 1))} onNextPage={() => setEventPage((page) => page + 1)} /> 1} canGoForward={operations.data?.hasNext ?? false} onPreviousPage={incomePagination.handlePrevious} onNextPage={() => incomePagination.handleNext(operations.data?.nextCursor)} /> ) } ``` - [ ] **Step 3: Export dashboard public API** Create `apps/frontend/src/widgets/broker-dashboard/index.ts`: ```ts export { BrokerDashboard } from './ui/BrokerDashboard' export { BrokerDashboardSkeleton } from './ui/BrokerDashboardSkeleton' ``` - [ ] **Step 4: Replace overview page composition** Modify `apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx` to: ```tsx import { Text } from '@moex-vibe/design-system' import { useBrokerAccountContext } from '@/widgets/broker-account-layout' import { BrokerDashboard, BrokerDashboardSkeleton } from '@/widgets/broker-dashboard' export function BrokerAccountOverviewPage() { const { accountId, portfolio } = useBrokerAccountContext() if (portfolio.isLoading) return if (portfolio.error || !portfolio.data) { return ( Не удалось загрузить сводку счёта ) } return } ``` ### Task 9: Dashboard Tests **Files:** - Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx` - [ ] **Step 1: Mock entity hooks and write composition test** Create `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx`: ```tsx import { render, screen } from '@testing-library/react' import { vi } from 'vitest' import type { BrokerPortfolio } from '@/shared/api' import { BrokerDashboard } from './BrokerDashboard' vi.mock('@/entities/broker-analytics', () => ({ useBrokerAnalytics: () => ({ data: { totalDeposits: 1000, totalWithdrawn: 100, netInvested: 900, totalDividends: 25, totalCoupons: 15, totalReceived: 40, totalReturnPercent: 4.44, currency: 'RUB', }, isLoading: false, isError: false, }), })) vi.mock('@/entities/broker-event', () => ({ useBrokerEvents: () => ({ data: { items: [], summary: {}, asOf: '2026-06-26T00:00:00.000Z' }, isLoading: false, isError: false, }), })) vi.mock('@/entities/broker-operation', () => ({ useBrokerOperations: () => ({ data: { accountId: 'acc-1', items: [], nextCursor: null, hasNext: false, asOf: '2026-06-26T00:00:00.000Z' }, isLoading: false, isError: false, }), })) vi.mock('@/widgets/broker-allocation-chart', () => ({ BrokerAllocationChart: () =>
allocation chart
, })) const portfolio: BrokerPortfolio = { account: { id: 'acc-1', name: 'Основной счёт', type: 'brokerage', status: 'open', openedAt: null, accessLevel: null }, positionCounts: { shares: 2, bonds: 1, etf: 0, other: 0 }, totals: { shares: null, bonds: null, etf: null, currencies: null, futures: null, options: null, structuredProducts: null, dfa: null, portfolio: { currency: 'RUB', units: '1000', nano: 0, value: 1000 }, }, yields: { expectedPercent: null, daily: null, dailyPercent: null }, cash: [], blockedCash: [], asOf: '2026-06-26T00:00:00.000Z', } describe('BrokerDashboard', () => { it('renders the dashboard sections', () => { render() expect(screen.getByText('Основной счёт')).toBeInTheDocument() expect(screen.getByText('События')).toBeInTheDocument() expect(screen.getByText('Доходы')).toBeInTheDocument() expect(screen.getByText('Аналитика доходности')).toBeInTheDocument() expect(screen.getByText('Аллокация')).toBeInTheDocument() expect(screen.getByText('allocation chart')).toBeInTheDocument() }) }) ``` - [ ] **Step 2: Run dashboard tests** Run: `rtk npm run test:frontend -- --run src/widgets/broker-dashboard` Expected: PASS. - [ ] **Step 3: Run all frontend tests** Run: `rtk npm run test:frontend` Expected: PASS. ### Task 10: Visual and Responsive Verification **Files:** - Modify only if verification reveals spacing or mobile readability issues in files created above. - [ ] **Step 1: Run frontend dev server** Run: `rtk npm run dev:frontend` Expected: Vite serves the app without compile errors. - [ ] **Step 2: Inspect desktop dashboard** Open `/broker/2084014113` and verify: hero appears first; events and income are side by side; analytics and allocation are below; detailed nav links remain available. - [ ] **Step 3: Inspect mobile dashboard** Resize to `390x844` and verify: the dashboard is one column; tables scroll horizontally only when necessary; navigation remains usable. ### Task 11: Final Quality Gate **Files:** - Modify: `docs/features/broker-dashboard-redesign/tasks.md` - [ ] **Step 1: Mark completed task checkboxes in docs** Update `docs/features/broker-dashboard-redesign/tasks.md` as tasks are completed. - [ ] **Step 2: Run affected package checks** Run: `rtk npm run test:frontend && rtk npm run test:design-system && rtk npm run lint -w apps/frontend && rtk npm run build:frontend` Expected: all commands pass. - [ ] **Step 3: Update graphify after code changes** Run: `graphify update .` Expected: graph update completes or reports no meaningful changes.