From b12c2741cd1770467f267adffbc53fa27bd0eab6 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 27 Jun 2026 13:44:31 +0300 Subject: [PATCH] feat(frontend): align analytics card with HTML parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Render RUB values via formatDashboardCurrency so RUB shows as ₽ instead of 'RUB', matching the hero, events and income cards. - Apply semantic color tones per spec §6: * Пополнения/Дивиденды/Купоны/Всего получено → positive when > 0, neutral when 0. * Выводы → prefix value with '−' and render with negative tone whenever the underlying amount is positive. * Нетто → sign-based tone. - Add data-testid and data-tone attributes on each analytics value so tests and downstream styling can address each metric. - Update the analytics mock in BrokerDashboard.test.tsx to cover positive, negative (net invested + withdrawals) and zero values, and add two new tests asserting ₽ rendering and the tone data attributes. --- .../ui/BrokerDashboard.test.tsx | 48 +++++- .../ui/BrokerDashboardAnalyticsCard.tsx | 139 ++++++++++++++++-- 2 files changed, 169 insertions(+), 18 deletions(-) diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx index 37d5cf0..06c4755 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboard.test.tsx @@ -22,11 +22,11 @@ vi.mock('@/entities/broker-analytics', () => ({ useBrokerAnalytics: () => ({ data: { totalDeposits: 1000, - totalWithdrawn: 100, - netInvested: 900, - totalDividends: 25, - totalCoupons: 15, - totalReceived: 40, + totalWithdrawn: 250, + netInvested: -150, + totalDividends: 75, + totalCoupons: 0, + totalReceived: 90, totalReturnPercent: 4.44, currency: 'RUB', }, @@ -252,6 +252,44 @@ describe('BrokerDashboard', () => { expect(heroText).not.toContain('RUB') }) + it('renders analytics card with the ₽ symbol and no "RUB" code', () => { + renderWithProviders() + + const analytics = screen.getByLabelText('Аналитика доходности') + const analyticsText = analytics.textContent ?? '' + expect(analyticsText).toContain('₽') + expect(analyticsText).not.toContain('RUB') + }) + + it('applies positive, negative and neutral tones to analytics values', () => { + renderWithProviders() + + const deposits = screen.getByTestId('dashboard-analytics-totalDeposits') + expect(deposits.getAttribute('data-tone')).toBe('positive') + expect(deposits.textContent).toContain('1\u00a0000,00') + expect(deposits.textContent).toContain('₽') + + const withdrawn = screen.getByTestId('dashboard-analytics-totalWithdrawn') + expect(withdrawn.getAttribute('data-tone')).toBe('negative') + expect(withdrawn.textContent).toMatch(/^[−-]250,00/) + + const net = screen.getByTestId('dashboard-analytics-netInvested') + expect(net.getAttribute('data-tone')).toBe('negative') + expect(net.textContent).toMatch(/^[−-]150,00/) + + const dividends = screen.getByTestId('dashboard-analytics-totalDividends') + expect(dividends.getAttribute('data-tone')).toBe('positive') + expect(dividends.textContent).toContain('75,00') + + const coupons = screen.getByTestId('dashboard-analytics-totalCoupons') + expect(coupons.getAttribute('data-tone')).toBe('neutral') + expect(coupons.textContent).toContain('0,00') + + const received = screen.getByTestId('dashboard-analytics-totalReceived') + expect(received.getAttribute('data-tone')).toBe('positive') + expect(received.textContent).toContain('90,00') + }) + it('shows skeleton table while events are loading', () => { hookMocks.useBrokerEvents.mockReturnValue({ data: undefined, diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx index 3ace3ad..28dd785 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx @@ -1,10 +1,33 @@ -import { Metric, Text } from '@moex-vibe/design-system' +import { Metric } from '@moex-vibe/design-system' import { Box } from '@mui/material' import type { BrokerAnalytics } from '@/shared/api' +import { + formatDashboardCurrency, + type MoneyTone, + moneyTone, + moneyToneToColor, +} from '../lib/dashboardVisual' import { BrokerDashboardCard } from './BrokerDashboardCard' -function amount(value: number, currency: string): string { - return `${value.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}` +type AnalyticsField = + | 'totalDeposits' + | 'totalWithdrawn' + | 'netInvested' + | 'totalDividends' + | 'totalCoupons' + | 'totalReceived' + +function analyticsTone(field: AnalyticsField, value: number): MoneyTone { + if (field === 'totalWithdrawn') { + return value > 0 ? 'negative' : moneyTone(value) + } + return moneyTone(value) +} + +function analyticsDisplay(field: AnalyticsField, value: number, currency: string): string { + const formatted = formatDashboardCurrency({ currency, value }) + if (field === 'totalWithdrawn' && value > 0) return `−${formatted}` + return formatted } export function BrokerDashboardAnalyticsCard({ @@ -17,13 +40,13 @@ export function BrokerDashboardAnalyticsCard({ isError: boolean }) { return ( - + {isError ? ( - Не удалось загрузить аналитику + ) : isLoading ? ( - Загрузка аналитики… + ) : !data ? ( - Нет данных для аналитики + ) : ( - - - - - - + + {analyticsDisplay('totalDeposits', data.totalDeposits, data.currency)} + + } + /> + + {analyticsDisplay('totalWithdrawn', data.totalWithdrawn, data.currency)} + + } + /> + + {analyticsDisplay('netInvested', data.netInvested, data.currency)} + + } + /> + + {analyticsDisplay('totalDividends', data.totalDividends, data.currency)} + + } + /> + + {analyticsDisplay('totalCoupons', data.totalCoupons, data.currency)} + + } + /> + + {analyticsDisplay('totalReceived', data.totalReceived, data.currency)} + + } + /> )}