From d804d4361615e7caf5e375129ee77e22e9798843 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 27 Jun 2026 10:45:20 +0300 Subject: [PATCH] docs: align broker dashboard redesign docs --- .../broker-dashboard-redesign/plan.md | 1139 +++-------------- .../broker-dashboard-redesign/spec.md | 33 +- .../broker-dashboard-redesign/tasks.md | 27 +- 3 files changed, 196 insertions(+), 1003 deletions(-) diff --git a/docs/features/broker-dashboard-redesign/plan.md b/docs/features/broker-dashboard-redesign/plan.md index 0d000c9..156fbae 100644 --- a/docs/features/broker-dashboard-redesign/plan.md +++ b/docs/features/broker-dashboard-redesign/plan.md @@ -1,988 +1,169 @@ -# 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. +> **Для агентных исполнителей:** ОБЯЗАТЕЛЬНЫЙ SUB-SKILL: использовать `superpowers:subagent-driven-development` (предпочтительно) или `superpowers:executing-plans` для пошагового выполнения. Шаги ведутся чекбоксами `- [ ]`. -**Goal:** Rebuild `/broker/:accountId` overview as a light-theme investment dashboard using current broker portfolio, events, operations, analytics, and allocation data. +**Цель:** превратить `/broker/:accountId` в компактный инвестиционный дашборд на текущей светлой теме без изменения backend-контрактов и URL-структуры. -**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. +**Архитектура:** маршрут и FSD-границы остаются прежними. Композиция собирается в `widgets/broker-dashboard`, данные продолжают приходить из существующих `entities/*` hooks. Навигация счёта остаётся в `BrokerAccountLayout`, а dashboard использует только локальные presentation/helpers без выноса брокерской логики в design system. -**Tech Stack:** React 18, TanStack Router, TanStack Query, MUI via `@moex-vibe/design-system`, Vitest, Testing Library. +**Технологии:** React 18, TanStack Router, TanStack Query, MUI через `@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. +- Modify: `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx` — горизонтальные вкладки над контентом обзора и подробных разделов. +- Modify: `apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx` — точка входа обзора через dashboard. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/index.ts` — публичный API dashboard-виджета. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` — верхнеуровневая композиция и управление filter state. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardHero.tsx` — hero KPI с fallback-значениями и выравниванием `Metric`. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx` — локальный паттерн карточки. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx` — карточка событий. +- Create: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx` — переиспользуемый фильтр дат с draft/apply поведением. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx` — карточка доходов. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx` — карточка аналитики доходности. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAllocationCard.tsx` — карточка аллокации с горизонтальными барами. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardSkeleton.tsx` — skeleton-форма dashboard. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx` — компонентные тесты композиции и фильтров. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.ts` — чистые helpers доходных операций. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.test.ts` — unit-тесты income helpers. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts` — пресеты дат, validate и mapping income types. +- Create/Modify: `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFormatters.ts` — локальные formatter/helpers для fallback и event labels. +- Modify: `docs/features/broker-dashboard-redesign/tasks.md` — фиксация статусов выполнения. + +### Источники данных + +- Портфель: `useBrokerAccountContext().portfolio`. +- События: `useBrokerEvents(accountId, { from, to, types })`. +- Операции: `useBrokerOperations(accountId, { from, to, operationTypes, cursor, limit: 10 })`. +- Аналитика: `useBrokerAnalytics(accountId)`. +- Аллокация: `buildBrokerAllocation(portfolio)`. + +## Технические решения + +### 1. Композиция страницы + +- Dashboard на desktop и mobile строится в одну колонку: `Hero`, `События`, `Доходы`, `Аналитика доходности`, `Аллокация`. +- Двухколоночный layout из прототипа не переносится. +- Навигация счёта находится над контентом в `BrokerAccountLayout` и не дублируется внутри dashboard. + +### 2. Hero KPI + +- Hero показывает название счёта, стоимость портфеля, доходность, дневное изменение и всего полученных доходов. +- Приоритет доходности: `analytics.totalReturnPercent`, затем `portfolio.yields.expectedPercent`, иначе `—`. +- Дневное изменение берётся из `portfolio.yields.daily` и `portfolio.yields.dailyPercent`, при недоступности показывается `—`. +- Все `Metric` должны иметь одинаковую высоту; supporting text не должен ломать вертикальный ритм. + +### 3. События + +- Типы событий переключаются chip-фильтрами с немедленным применением. +- Диапазон дат редактируется отдельно от применённого состояния: draft state меняется локально, запрос уходит только по `Показать`. +- При пустом выборе типов запрос отключается, карточка показывает валидационное сообщение. +- Пагинация локальная, по 10 событий на страницу, сбрасывается при смене применённых фильтров. + +### 4. Доходы + +- Доходы строятся на существующем endpoint операций только для `OPERATION_TYPE_DIVIDEND`, `OPERATION_TYPE_DIV_EXT`, `OPERATION_TYPE_COUPON`. +- Типы доходов переключаются chip-фильтрами с немедленным применением. +- Диапазон дат использует тот же draft/apply паттерн, что и события. +- Пагинация cursor-based, размер страницы 10, сбрасывается при смене применённых фильтров. + +### 5. Аналитика и аллокация + +- Карточка аналитики использует существующий analytics endpoint и показывает спокойное empty state при отсутствии данных. +- Карточка аллокации не использует donut chart. Она строит список горизонтальных bar rows по `buildBrokerAllocation`. +- Отрицательные значения показываются текстом без полосы. + +### 6. Ошибки и пустые состояния + +- Ошибка `portfolio` роняет весь обзор. +- Ошибки `events`, `income`, `analytics` локальны соответствующим карточкам. +- Пустые данные показываются отдельными сообщениями, а не нулевыми значениями. + +## Задачи + +### Задача 1: Базовые helpers и локальные dashboard-patterns + +**Файлы:** +- `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.ts` +- `apps/frontend/src/widgets/broker-dashboard/lib/dashboardIncome.test.ts` +- `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFilters.ts` +- `apps/frontend/src/widgets/broker-dashboard/lib/dashboardFormatters.ts` +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx` + +- [ ] Убедиться, что income helpers покрывают только допустимые типы доходов и умеют считать итог отображаемых строк. +- [ ] Убедиться, что filter helpers содержат default state, date presets, validate и mapping income type -> operation types. +- [ ] Убедиться, что formatters покрывают fallback-значения, label типов событий и label статусов. +- [ ] Использовать локальный `BrokerDashboardCard` как основной контейнер карточек; design system расширять только если без этого нельзя реализовать требования спецификации. + +### Задача 2: Hero и layout overview + +**Файлы:** +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardHero.tsx` +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardSkeleton.tsx` +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` +- `apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx` +- `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx` + +- [ ] Собрать обзор через `BrokerDashboard` и `BrokerDashboardSkeleton`. +- [ ] Оставить навигацию счёта в `BrokerAccountLayout` как горизонтальные вкладки над контентом. +- [ ] Исправить hero так, чтобы все `Metric` были одной высоты и поддерживающий текст не поднимал одну ячейку выше остальных. +- [ ] Проверить, что dashboard остаётся одноколоночным и на desktop, и на mobile. + +### Задача 3: Карточка событий + +**Файлы:** +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardDateFilter.tsx` +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx` +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` + +- [ ] Добавить переиспользуемый `BrokerDashboardDateFilter` с preset chips, полями `from/to`, действиями `Сбросить` и `Показать`. +- [ ] Подключить для событий draft/applied state: типы применяются сразу, даты только по `Показать`. +- [ ] Сохранять локальную пагинацию по 10 событий и сбрасывать её при смене применённых фильтров. +- [ ] Оставить локальные loading/error/empty states внутри карточки. + +### Задача 4: Карточка доходов + +**Файлы:** +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardIncomeCard.tsx` +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.tsx` + +- [ ] Подключить тот же `BrokerDashboardDateFilter` для доходов с draft/applied state. +- [ ] Оставить chip-фильтры типов доходов с немедленным применением. +- [ ] Сохранить cursor pagination по 10 операций и сбрасывать её при смене применённых фильтров. +- [ ] Если endpoint операций даёт недостаточно релевантных строк для dashboard, зафиксировать ограничение в заметках по реализации, а не расширять backend в рамках этой фичи. + +### Задача 5: Карточки аналитики и аллокации + +**Файлы:** +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx` +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAllocationCard.tsx` + +- [ ] Довести карточку аналитики до соответствия спецификации по полям и состояниям. +- [ ] Заменить donut chart на горизонтальные бары, построенные из `buildBrokerAllocation`. +- [ ] Отрицательные значения аллокации выводить отдельно текстом без bar. + +### Задача 6: Тесты и верификация + +**Файлы:** +- `apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx` +- `docs/features/broker-dashboard-redesign/tasks.md` + +- [ ] Обновить компонентные тесты dashboard так, чтобы они покрывали текущую композицию, фильтры и основные empty/error states. +- [ ] Прогнать `rtk npm run test:frontend -- --run src/widgets/broker-dashboard`. +- [ ] Прогнать `rtk npm run test:frontend`. +- [ ] Прогнать `rtk npm run test:design-system && rtk npm run lint -w apps/frontend && rtk npm run build:frontend`. +- [ ] Проверить вручную desktop layout `/broker/2084014113`. +- [ ] Проверить вручную mobile layout `/broker/2084014113` на viewport `390x844`. +- [ ] После завершения обновить `docs/features/broker-dashboard-redesign/tasks.md` и выполнить `graphify update .`. + +## Проверка покрытия спецификации + +- Общая композиция и горизонтальная навигация: задачи 2 и 6. +- Hero KPI и equal-height `Metric`: задача 2. +- События с chip filters, date filters и локальной пагинацией: задача 3. +- Доходы с chip filters, date filters и cursor pagination: задача 4. +- Аналитика и аллокация с горизонтальными барами: задача 5. +- Ошибки, empty states, тесты и ручная верификация: задача 6. diff --git a/docs/features/broker-dashboard-redesign/spec.md b/docs/features/broker-dashboard-redesign/spec.md index 272c0b5..1fd5590 100644 --- a/docs/features/broker-dashboard-redesign/spec.md +++ b/docs/features/broker-dashboard-redesign/spec.md @@ -1,6 +1,6 @@ -# Редизайн overview брокерского счёта в инвестиционный дашборд +# Редизайн обзора брокерского счёта в инвестиционный дашборд -Дата: 2026-06-26 +Дата: 2026-06-26 (обновлено 2026-06-27) Статус: согласовано к планированию Эпик: [Портфель брокера](../../epics/BrokerPortfolio.md) @@ -9,12 +9,14 @@ Текущий маршрут `/broker/:accountId` показывает корректный overview выбранного брокерского счёта, но визуально остаётся вертикальным набором отдельных блоков: сводка, аллокация, карточки активов, ближайшие события и последние операции. Пользователь подготовил прототип `temp.html`, где тот же домен -представлен как более плотный инвестиционный дашборд: hero KPI, события и доходы рядом, аналитика и -аллокация в нижнем ряду. +представлен как более плотный инвестиционный дашборд с hero KPI, компактными таблицами и аналитическими +карточками. После обсуждения выбран вариант A: страница `/broker/:accountId` должна стать единым дашбордом, используя структуру и плотность прототипа, но сохраняя текущую светлую тему и компоненты -`@moex-vibe/design-system`. Тёмная тема из прототипа не переносится в первую версию. +`@moex-vibe/design-system`. Тёмная тема из прототипа не переносится в первую версию. При этом +двухколоночная desktop-композиция прототипа сознательно не переносится: и на desktop, и на mobile блоки +dashboard идут по одному на строке в фиксированном порядке. ## Цель @@ -29,13 +31,13 @@ - увидеть ближайшие события по счёту в компактной таблице; - увидеть последние доходные операции по дивидендам и купонам и итог по ним; - оценить вложения, полученные выплаты и доходность по существующей аналитике; -- увидеть структуру портфеля через donut-диаграмму и легенду; +- увидеть структуру портфеля через горизонтальные полосы аллокации и подписи к ним; - перейти в существующие подробные вкладки `Акции`, `Облигации`, `Операции`, `События` и `Аналитика` для drill-down сценариев. ## Область изменений -Фича относится только к frontend маршруту `/broker/:accountId` и связанным frontend-компонентам +Фича относится только к frontend-маршруту `/broker/:accountId` и связанным frontend-компонентам брокерского overview. В область входят: @@ -82,6 +84,7 @@ Hero показывает: - дневное изменение из `portfolio.yields.daily` и `portfolio.yields.dailyPercent`, если доступно; - всего полученных доходов из `broker analytics.totalReceived`, если аналитика загружена; - спокойный fallback `—` для недоступных значений. +- Все Metric-блоки имеют одинаковую высоту в grid-ряду, даже если у некоторых есть supportingText. Поддерживающий текст остаётся внутри Metric, но все Metrics растягиваются на полную высоту grid-ячейки с выравниванием от верхнего края. ### 4. Блок `События` @@ -144,10 +147,14 @@ Hero показывает: ### 7. Блок `Аллокация` -- Используется существующий расчёт `buildBrokerAllocation` и текущая `BrokerAllocationChart` либо её - dashboard-адаптация. -- Блок показывает итоговую стоимость портфеля и ненулевые секторы с названием, суммой и процентом. -- Отрицательные значения отображаются текстом, а не сектором диаграммы. +- Используется существующий расчёт `buildBrokerAllocation`. +- Вместо donut-диаграммы используется горизонтальный bar chart: каждый сектор — полоса с процентом, + подписью и суммой. +- Сверху блока показывается итоговая стоимость портфеля. +- Каждая полоса содержит: название сектора, долю в процентах, сумму в валюте. +- Цвета полос соответствуют существующей палитре аллокации (акции, облигации, ETF, деньги, прочие). +- Отрицательные значения отображаются текстом без полосы. +- Текст подписей контрастный и читаемый на всех цветах фона. - Информация остаётся понятной без различения цветов. ### 8. Загрузка, ошибки и пустые состояния @@ -184,12 +191,14 @@ Hero показывает: - На мобильном viewport dashboard читаемо перестраивается в одну колонку. - Hero показывает стоимость портфеля, доходность или fallback, дневное изменение или fallback, всего доходов или fallback. +- Все Metric-блоки hero имеют одинаковую высоту; supportingText не создаёт перекоса. - Блок `События` использует существующие events data и показывает дату, инструмент, тип, сумму и статус. - Блок `События` поддерживает multi-select chip-фильтр типов и локальную пагинацию по 10 событий. - Блок `Доходы` показывает доходные операции дивидендов и купонов и итог по отображаемым строкам. - Блок `Доходы` поддерживает multi-select chip-фильтр типов и cursor-пагинацию по 10 операций. - Блок `Аналитика доходности` показывает данные существующего analytics endpoint. -- Блок `Аллокация` показывает donut/легенду существующей структуры портфеля. +- Блок `Аллокация` показывает горизонтальные бары секторов с названием, долей и суммой, а также + итоговую стоимость портфеля. - Ошибка одного вторичного блока не скрывает остальные блоки dashboard. - Существующие detailed вкладки остаются доступны из навигации счёта. - Первая версия не содержит график истории стоимости портфеля и не добавляет API для него. diff --git a/docs/features/broker-dashboard-redesign/tasks.md b/docs/features/broker-dashboard-redesign/tasks.md index f8912ab..13a6842 100644 --- a/docs/features/broker-dashboard-redesign/tasks.md +++ b/docs/features/broker-dashboard-redesign/tasks.md @@ -1,7 +1,7 @@ -# Редизайн overview брокерского счёта в инвестиционный дашборд — задачи +# Редизайн обзора брокерского счёта в инвестиционный дашборд — задачи Дата: 2026-06-26 -Статус: готово к реализации +Статус: в реализации ## Документация и pre-flight @@ -26,26 +26,29 @@ - [x] Перенести навигацию счёта из левой колонки в горизонтальные вкладки над контентом. - [x] Перестроить dashboard на один блок на строке для `События`, `Доходы`, `Аналитика доходности`, `Аллокация`. - [x] Добавить кликабельные chip-фильтры типов для `События`. -- [ ] Добавить фильтры периода, пресеты, reset/apply actions для `События`. +- [ ] Добавить `BrokerDashboardDateFilter` — переиспользуемый expandable-компонент фильтра дат (пресеты 7д/30д/90д/1г/Всё, from/to поля, Сбросить/Показать). +- [ ] Подключить `BrokerDashboardDateFilter` в `События` с draft/applied состоянием. - [x] Добавить локальную пагинацию по 10 событий в `События`. - [x] Добавить кликабельные chip-фильтры типов для `Доходы`. -- [ ] Добавить фильтры периода, пресеты, reset/apply actions для `Доходы`. +- [ ] Подключить `BrokerDashboardDateFilter` в `Доходы` с draft/applied состоянием. - [x] Добавить cursor-пагинацию по 10 операций в `Доходы`. - [x] Добавить `BrokerDashboardAnalyticsCard` на основе `useBrokerAnalytics`. - [x] Добавить `BrokerDashboardAllocationCard` на основе существующей аллокации. - [x] Добавить `BrokerDashboardSkeleton`. - [x] Добавить `BrokerDashboard` как top-level composition widget. -- [x] Заменить текущий vertical overview в `BrokerAccountOverviewPage` на `BrokerDashboard`. -- [x] Добавить unit/component tests для helpers и dashboard composition. +- [x] Заменить текущий вертикальный обзор в `BrokerAccountOverviewPage` на `BrokerDashboard`. +- [x] Добавить unit/component tests для helpers и базовой dashboard composition. +- [ ] Выровнять hero KPI: все Metric одной высоты, supportingText не раздвигает "Доходность" выше соседей. +- [ ] Заменить donut-диаграмму аллокации на горизонтальные бары в `BrokerDashboardAllocationCard`. - [ ] Проверить desktop layout `/broker/2084014113`. - [ ] Проверить mobile layout `/broker/2084014113`. ## Definition of Done -- [x] `rtk npm run test:frontend` проходит (31 files, 135 tests). -- [x] `rtk npm run test:design-system` проходит (28 files, 162 tests). -- [x] `rtk npm run lint -w apps/frontend` проходит. -- [x] `rtk npm run build:frontend` проходит. -- [x] Dashboard соответствует acceptance criteria из `spec.md`. +- [ ] `rtk npm run test:frontend` проходит. +- [ ] `rtk npm run test:design-system` проходит. +- [ ] `rtk npm run lint -w apps/frontend` проходит. +- [ ] `rtk npm run build:frontend` проходит. +- [ ] Dashboard соответствует acceptance criteria из `spec.md`. - [x] Существующие вкладки `Акции`, `Облигации`, `Операции`, `События`, `Аналитика` остаются доступны. -- [x] `graphify update .` выполнен после code changes. +- [ ] `graphify update .` выполнен после code changes.