feat(frontend): align analytics card with HTML parity
- 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.
This commit is contained in:
parent
a1377fa5c2
commit
b12c2741cd
@ -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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||
|
||||
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(<BrokerDashboard accountId="acc-1" portfolio={portfolio} />)
|
||||
|
||||
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,
|
||||
|
||||
@ -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 (
|
||||
<BrokerDashboardCard title="Аналитика доходности">
|
||||
<BrokerDashboardCard title="Аналитика доходности" ariaLabel="Аналитика доходности">
|
||||
{isError ? (
|
||||
<Text tone="negative">Не удалось загрузить аналитику</Text>
|
||||
<Metric label="Ошибка" value="Не удалось загрузить аналитику" />
|
||||
) : isLoading ? (
|
||||
<Text tone="muted">Загрузка аналитики…</Text>
|
||||
<Metric label="Загрузка" value="Загрузка аналитики…" />
|
||||
) : !data ? (
|
||||
<Text tone="muted">Нет данных для аналитики</Text>
|
||||
<Metric label="Нет данных" value="Нет данных для аналитики" />
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
@ -32,12 +55,102 @@ export function BrokerDashboardAnalyticsCard({
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Metric label="Пополнения" value={amount(data.totalDeposits, data.currency)} />
|
||||
<Metric label="Выводы" value={`−${amount(data.totalWithdrawn, data.currency)}`} />
|
||||
<Metric label="Нетто" value={amount(data.netInvested, data.currency)} />
|
||||
<Metric label="Дивиденды" value={amount(data.totalDividends, data.currency)} />
|
||||
<Metric label="Купоны" value={amount(data.totalCoupons, data.currency)} />
|
||||
<Metric label="Всего получено" value={amount(data.totalReceived, data.currency)} />
|
||||
<Metric
|
||||
label="Пополнения"
|
||||
value={
|
||||
<Box
|
||||
component="span"
|
||||
data-testid="dashboard-analytics-totalDeposits"
|
||||
data-tone={analyticsTone('totalDeposits', data.totalDeposits)}
|
||||
sx={{
|
||||
color: moneyToneToColor(analyticsTone('totalDeposits', data.totalDeposits)),
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{analyticsDisplay('totalDeposits', data.totalDeposits, data.currency)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Metric
|
||||
label="Выводы"
|
||||
value={
|
||||
<Box
|
||||
component="span"
|
||||
data-testid="dashboard-analytics-totalWithdrawn"
|
||||
data-tone={analyticsTone('totalWithdrawn', data.totalWithdrawn)}
|
||||
sx={{
|
||||
color: moneyToneToColor(analyticsTone('totalWithdrawn', data.totalWithdrawn)),
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{analyticsDisplay('totalWithdrawn', data.totalWithdrawn, data.currency)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Metric
|
||||
label="Нетто"
|
||||
value={
|
||||
<Box
|
||||
component="span"
|
||||
data-testid="dashboard-analytics-netInvested"
|
||||
data-tone={analyticsTone('netInvested', data.netInvested)}
|
||||
sx={{
|
||||
color: moneyToneToColor(analyticsTone('netInvested', data.netInvested)),
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{analyticsDisplay('netInvested', data.netInvested, data.currency)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Metric
|
||||
label="Дивиденды"
|
||||
value={
|
||||
<Box
|
||||
component="span"
|
||||
data-testid="dashboard-analytics-totalDividends"
|
||||
data-tone={analyticsTone('totalDividends', data.totalDividends)}
|
||||
sx={{
|
||||
color: moneyToneToColor(analyticsTone('totalDividends', data.totalDividends)),
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{analyticsDisplay('totalDividends', data.totalDividends, data.currency)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Metric
|
||||
label="Купоны"
|
||||
value={
|
||||
<Box
|
||||
component="span"
|
||||
data-testid="dashboard-analytics-totalCoupons"
|
||||
data-tone={analyticsTone('totalCoupons', data.totalCoupons)}
|
||||
sx={{
|
||||
color: moneyToneToColor(analyticsTone('totalCoupons', data.totalCoupons)),
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{analyticsDisplay('totalCoupons', data.totalCoupons, data.currency)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Metric
|
||||
label="Всего получено"
|
||||
value={
|
||||
<Box
|
||||
component="span"
|
||||
data-testid="dashboard-analytics-totalReceived"
|
||||
data-tone={analyticsTone('totalReceived', data.totalReceived)}
|
||||
sx={{
|
||||
color: moneyToneToColor(analyticsTone('totalReceived', data.totalReceived)),
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{analyticsDisplay('totalReceived', data.totalReceived, data.currency)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</BrokerDashboardCard>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user