Replace vertical overview with dashboard composition: - BrokerDashboardHero with portfolio KPI, return, daily change, total income - BrokerDashboardEventsCard with compact events table and local pagination - BrokerDashboardIncomeCard with income operations table and cursor pagination - BrokerDashboardAnalyticsCard with analytics Metric grid - BrokerDashboardAllocationCard wrapping existing donut chart - BrokerDashboardSkeleton matching dashboard shape - BrokerDashboardCard local card pattern - Pure lib helpers: dashboardIncome, dashboardFilters, dashboardFormatters - Unit tests for income helpers (6) and dashboard composition (1)
118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
import { Button, Chip, Text } from '@moex-vibe/design-system'
|
||
import { Box } from '@mui/material'
|
||
import { Link } from '@tanstack/react-router'
|
||
import type { BrokerOperationsPage } from '@/shared/api'
|
||
import { formatBrokerCurrencyValue, formatBrokerDate } from '@/shared/lib/formatters'
|
||
import { getDashboardIncomeRows, sumDashboardIncome } from '../lib/dashboardIncome'
|
||
import { BrokerDashboardCard } from './BrokerDashboardCard'
|
||
|
||
type BrokerDashboardIncomeCardProps = {
|
||
accountId: string
|
||
page: BrokerOperationsPage | undefined
|
||
isLoading: boolean
|
||
isError: boolean
|
||
pageNumber: number
|
||
canGoBack: boolean
|
||
canGoForward: boolean
|
||
onPreviousPage: () => void
|
||
onNextPage: () => void
|
||
}
|
||
|
||
export function BrokerDashboardIncomeCard({
|
||
accountId,
|
||
page,
|
||
isLoading,
|
||
isError,
|
||
pageNumber,
|
||
canGoBack,
|
||
canGoForward,
|
||
onPreviousPage,
|
||
onNextPage,
|
||
}: BrokerDashboardIncomeCardProps) {
|
||
const rows = getDashboardIncomeRows(page?.items ?? []).slice(0, 10)
|
||
const total = sumDashboardIncome(rows)
|
||
|
||
return (
|
||
<BrokerDashboardCard
|
||
title="Доходы"
|
||
action={<Link to={`/broker/${encodeURIComponent(accountId)}/operations`}>Все операции</Link>}
|
||
>
|
||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 1.5 }}>
|
||
<Chip label="Дивиденды" tone="success" />
|
||
<Chip label="Купоны" tone="info" />
|
||
</Box>
|
||
{isError ? (
|
||
<Text tone="negative">Не удалось загрузить доходные операции</Text>
|
||
) : isLoading ? (
|
||
<Text tone="muted">Загрузка доходов…</Text>
|
||
) : rows.length === 0 ? (
|
||
<Text tone="muted">Дивидендов и купонов в последних операциях нет</Text>
|
||
) : (
|
||
<Box sx={{ display: 'grid', gap: 1 }}>
|
||
<Box sx={{ overflowX: 'auto' }}>
|
||
<Box component="table" sx={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
|
||
<Box component="tbody">
|
||
{rows.map((row) => (
|
||
<Box component="tr" key={row.id}>
|
||
<Box
|
||
component="td"
|
||
sx={{ py: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||
>
|
||
{formatBrokerDate(row.date)}
|
||
</Box>
|
||
<Box
|
||
component="td"
|
||
sx={{
|
||
py: 1,
|
||
borderBottom: '1px solid',
|
||
borderColor: 'divider',
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{row.instrument}
|
||
</Box>
|
||
<Box
|
||
component="td"
|
||
sx={{ py: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||
>
|
||
{row.typeLabel}
|
||
</Box>
|
||
<Box
|
||
component="td"
|
||
sx={{
|
||
py: 1,
|
||
borderBottom: '1px solid',
|
||
borderColor: 'divider',
|
||
textAlign: 'right',
|
||
fontWeight: 700,
|
||
color: 'success.main',
|
||
}}
|
||
>
|
||
+{formatBrokerCurrencyValue(row.amount.currency, row.amount.value)}
|
||
</Box>
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
<Text variant="caption" tone="secondary">
|
||
Показано: {rows.length} · Итого:{' '}
|
||
{total ? formatBrokerCurrencyValue(total.currency, total.value) : '—'}
|
||
</Text>
|
||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, alignItems: 'center' }}>
|
||
<Button variant="secondary" size="small" onClick={onPreviousPage} disabled={!canGoBack}>
|
||
←
|
||
</Button>
|
||
<Text variant="body" tone="secondary">
|
||
{pageNumber}
|
||
</Text>
|
||
<Button variant="secondary" size="small" onClick={onNextPage} disabled={!canGoForward}>
|
||
→
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</BrokerDashboardCard>
|
||
)
|
||
}
|