diff --git a/apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx b/apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx index 779f5d0..7c76cc3 100644 --- a/apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx +++ b/apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx @@ -6,7 +6,7 @@ import { createContext, type ReactNode } from 'react' import { useBrokerPortfolio } from '@/entities/broker-account' import type { BrokerPortfolio } from '@/shared/api' import { formatBrokerMoney, formatBrokerPercent } from '@/shared/lib/formatters' -import { moneyTone, moneyToneToColor } from '@/widgets/broker-dashboard/lib/dashboardVisual' +import { moneyTone } from '@/widgets/broker-dashboard/lib/dashboardVisual' const baseLinkStyle: React.CSSProperties = { padding: '10px 14px', @@ -45,27 +45,67 @@ export function BrokerAccountLayout({ children }: { children: ReactNode }) { return ( - - {portfolio.data?.account.name || 'Брокерский счёт'} - {portfolio.data ? ( - - - {formatBrokerPercent(portfolio.data.yields.expectedPercent as number | null)} + + + + {portfolio.data?.account.name || 'Брокерский счёт'} + + {portfolio.data ? ( + + + Доходность + + + {formatBrokerPercent(portfolio.data.yields.expectedPercent as number | null)} + + + За день:{' '} + + {formatBrokerMoney(portfolio.data.yields.daily)} + + - - За день: {formatBrokerMoney(portfolio.data.yields.daily)} - - - ) : null} + ) : null} + diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx index 080816d..471a82a 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardAnalyticsCard.tsx @@ -1,12 +1,7 @@ -import { Metric, Text } from '@moex-vibe/design-system' +import { Text } 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 type { BrokerAnalytics, BrokerMoney } from '@/shared/api' +import { formatDashboardCurrency, type MoneyTone, moneyTone } from '../lib/dashboardVisual' import { BrokerDashboardCard } from './BrokerDashboardCard' type AnalyticsField = @@ -54,10 +49,12 @@ function analyticsDisplay(field: AnalyticsField, value: number, currency: string export function BrokerDashboardAnalyticsCard({ data, + portfolioValue, isLoading, isError, }: { data: BrokerAnalytics | undefined + portfolioValue?: BrokerMoney isLoading: boolean isError: boolean }) { @@ -70,48 +67,137 @@ export function BrokerDashboardAnalyticsCard({ ) : !data ? ( Нет данных для аналитики ) : ( - - {ANALYTICS_METRICS.map(({ field, label, testId }) => { - const value = data[field] - const tone = analyticsTone(field, value) - return ( + + + + + Стоимость портфеля + + + {formatDashboardCurrency({ + currency: portfolioValue?.currency ?? data.currency, + value: portfolioValue?.value ?? 0, + })} + + + + + Всего доходов + - - {analyticsDisplay(field, value, data.currency)} - - } - /> + {formatDashboardCurrency({ + currency: data.currency, + value: data.totalDividends + data.totalCoupons, + })} - ) - })} + + + + {ANALYTICS_METRICS.map(({ field, label, testId }) => { + const value = data[field] + const tone = analyticsTone(field, value) + return ( + + + {label} + + + {analyticsDisplay(field, value, data.currency)} + + + ) + })} + )} diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx index 0cbe672..eea01fa 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardCard.tsx @@ -47,7 +47,11 @@ export function BrokerDashboardCard({ }} > - + {title} {badge ? ( diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx index 82d7827..40992bd 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerDashboardEventsCard.tsx @@ -1,6 +1,7 @@ -import { Chip, Text } from '@moex-vibe/design-system' +import { Text } from '@moex-vibe/design-system' import { Box } from '@mui/material' import { Link } from '@tanstack/react-router' +import { useState } from 'react' import type { BrokerOperation } from '@/shared/api' import { formatBrokerDate } from '@/shared/lib/formatters' import { formatDashboardCurrency, moneyToneToColor } from '../lib/dashboardVisual' @@ -38,6 +39,18 @@ const TD_SX_INSTRUMENT = { minWidth: 180, } +const ITEMS_PER_PAGE = 7 + +const BADGE_STYLES: Record = { + coupon: { bg: '#e8f0ff', borderColor: '#cddcff', color: '#2563eb' }, + dividend: { bg: '#e5f2ea', borderColor: '#bcdcc9', color: '#176747' }, + maturity: { bg: '#fff5dc', borderColor: '#f0d898', color: '#8d6400' }, + tax: { bg: '#fef0ef', borderColor: '#f6c6c2', color: '#b42318' }, + fee: { bg: '#fef0ef', borderColor: '#f6c6c2', color: '#b42318' }, +} + +type BadgeInfo = { label: string; badgeKey: string } + type BrokerDashboardEventsCardProps = { accountId: string data: BrokerOperation[] | undefined @@ -45,29 +58,94 @@ type BrokerDashboardEventsCardProps = { isError: boolean } -function getOperationBadge(operation: BrokerOperation): { label: string } { - if (operation.category === 'tax') return { label: 'Налог' } - if (operation.category === 'fee') return { label: 'Комиссия' } +function getOperationBadge(operation: BrokerOperation): BadgeInfo { + if (operation.category === 'tax') return { label: 'Налог', badgeKey: 'tax' } + if (operation.category === 'fee') return { label: 'Комиссия', badgeKey: 'fee' } if (operation.category === 'income') { if ( operation.type === 'OPERATION_TYPE_DIVIDEND' || operation.type === 'OPERATION_TYPE_DIV_EXT' ) { - return { label: 'Дивиденд' } + return { label: 'Дивиденд', badgeKey: 'dividend' } } if (operation.type === 'OPERATION_TYPE_COUPON') { - return { label: 'Купон' } + return { label: 'Купон', badgeKey: 'coupon' } } if ( operation.type === 'OPERATION_TYPE_BOND_REPAYMENT' || operation.type === 'OPERATION_TYPE_BOND_REPAYMENT_FULL' || operation.type === 'OPERATION_TYPE_MATURITY' ) { - return { label: 'Погашение' } + return { label: 'Погашение', badgeKey: 'maturity' } } - return { label: 'Доход' } + return { label: 'Доход', badgeKey: '' } } - return { label: 'Прочее' } + return { label: 'Прочее', badgeKey: '' } +} + +function Badge({ label, badgeKey }: { label: string; badgeKey: string }) { + const style = BADGE_STYLES[badgeKey] + return ( + + {label} + + ) +} + +function PagerButton({ + disabled, + label, + onClick, +}: { + disabled?: boolean + label: string + onClick?: () => void +}) { + return ( + + {label} + + ) } export function BrokerDashboardEventsCard({ @@ -76,7 +154,13 @@ export function BrokerDashboardEventsCard({ isLoading, isError, }: BrokerDashboardEventsCardProps) { + const [page, setPage] = useState(0) const operations = data ?? [] + const totalPages = Math.max(1, Math.ceil(operations.length / ITEMS_PER_PAGE)) + const safePage = Math.min(page, totalPages - 1) + const pageStart = safePage * ITEMS_PER_PAGE + const pageEnd = pageStart + ITEMS_PER_PAGE + const pageItems = operations.slice(pageStart, pageEnd) return ( - {operations.slice(0, 7).map((op) => { + {pageItems.map((op) => { const boldText = op.name ?? op.description ?? op.ticker ?? '—' const grayText = op.name ? (op.ticker ?? null) : null - const { label } = getOperationBadge(op) + const { label, badgeKey } = getOperationBadge(op) const amountValue = op.payment?.value ?? 0 const amountTone = amountValue >= 0 ? 'positive' : 'negative' const formattedAmount = `${amountValue >= 0 ? '+' : '\u2212'}${formatDashboardCurrency( @@ -152,7 +236,7 @@ export function BrokerDashboardEventsCard({ - + + + + setPage((p) => Math.max(0, p - 1))} + /> + + {safePage + 1} + + = totalPages - 1} + label="→" + onClick={() => setPage((p) => p + 1)} + /> + + )} diff --git a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerPortfolioHistoryCard.tsx b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerPortfolioHistoryCard.tsx index 44d0557..734b528 100644 --- a/apps/frontend/src/widgets/broker-dashboard/ui/BrokerPortfolioHistoryCard.tsx +++ b/apps/frontend/src/widgets/broker-dashboard/ui/BrokerPortfolioHistoryCard.tsx @@ -19,11 +19,11 @@ function generateChartPath(points: { value: BrokerMoney }[]): { line: string; ar const padding = range * 0.1 const viewWidth = 300 - const viewHeight = 120 + const viewHeight = 90 const padLeft = 10 const padRight = 10 const padTop = 5 - const padBottom = 20 + const padBottom = 5 const plotWidth = viewWidth - padLeft - padRight const plotHeight = viewHeight - padTop - padBottom const yMin = min - padding @@ -48,7 +48,7 @@ function generateChartPath(points: { value: BrokerMoney }[]): { line: string; ar lineCmd += ` C ${cx1},${y0} ${cx2},${y1} ${x1},${y1}` } - const bottom = viewHeight - padBottom + const bottom = viewHeight const areaCmd = `${lineCmd} L ${x(points.length - 1)},${bottom} L ${x(0)},${bottom} Z` return { line: lineCmd, area: areaCmd } @@ -65,7 +65,7 @@ export function BrokerPortfolioHistoryCard({ title="Стоимость портфеля за 6 месяцев" sx={{ borderColor: 'success.light', - bgcolor: 'rgba(46, 125, 50, 0.06)', + background: 'linear-gradient(135deg, rgba(229, 242, 234, 0.96), rgba(255, 255, 255, 0.86))', }} > {(() => { @@ -74,7 +74,7 @@ export function BrokerPortfolioHistoryCard({ } if (isLoading || !data) { - return + return } const points = data.points @@ -86,50 +86,82 @@ export function BrokerPortfolioHistoryCard({ return ( - - Текущая стоимость:{' '} - - {portfolioValue ? formatDashboardCurrency(portfolioValue) : '—'} - - - - - - - - - - - - {points.map((point, i) => { - const plotWidth = 300 - 10 - 10 - const px = 10 + (i / (points.length - 1)) * plotWidth - return ( - - {point.label} - - ) - })} - + + + Текущая стоимость + + + {portfolioValue ? formatDashboardCurrency(portfolioValue) : '—'} + + + + + + + + + + + + + + + + {points.map((point, i) => { + const plotWidth = 300 - 10 - 10 + const leftPct = ((10 + (i / (points.length - 1)) * plotWidth) / 300) * 100 + const isFirst = i === 0 + const isLast = i === points.length - 1 + return ( + + {point.label} + + ) + })} + + ) })()}