import { useState } from 'react'; import { Link } from 'react-router-dom'; import type { BrokerMoney, BrokerPosition } from '../../api/responses'; import { getBrokerInstrumentPath } from './brokerDisplay'; import { TableSkeleton } from '../../components/TableSkeleton'; import { useBrokerPositions } from '../../hooks/useBrokerPositions'; type BrokerPositionGroupConfig = { key: string; type?: string; title: string; }; const GROUPS: BrokerPositionGroupConfig[] = [ { key: 'shares', type: 'share', title: 'Акции' }, { key: 'bonds', type: 'bond', title: 'Облигации' }, { key: 'etf', type: 'etf', title: 'ETF' }, { key: 'fund', type: 'fund', title: 'Фонды' }, ]; const KNOWN_TYPES = new Set(GROUPS.map((g) => g.type).filter(Boolean)); const tableStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 14, } satisfies React.CSSProperties; const thStyle = { borderBottom: '1px solid #e0e0e0', color: 'var(--color-text-secondary)', fontWeight: 600, padding: '10px 8px', } satisfies React.CSSProperties; const tdStyle = { borderBottom: '1px solid #eeeeee', padding: '10px 8px', verticalAlign: 'top', } satisfies React.CSSProperties; const pagButtonStyle = { padding: '6px 14px', borderRadius: 6, border: '1px solid #e0e0e0', background: 'var(--color-surface)', color: 'var(--color-text)', fontSize: 14, fontWeight: 600, cursor: 'pointer', lineHeight: 1.4, } satisfies React.CSSProperties; const pagButtonDisabledStyle = { ...pagButtonStyle, opacity: 0.35, cursor: 'not-allowed', } satisfies React.CSSProperties; function formatMoney(value: BrokerMoney | null | undefined) { if (!value) return '-'; return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: value.currency || 'RUB', maximumFractionDigits: 2, }).format(value.value); } function formatQuantity(value: number | null | undefined) { return value == null ? '-' : value.toLocaleString('ru-RU'); } function PositionTicker({ position }: { position: BrokerPosition }) { const label = position.ticker || position.figi || '-'; const path = getBrokerInstrumentPath({ ticker: position.ticker, instrumentType: position.instrumentType, classCode: position.classCode, }); if (!path || label === '-') { return {label}; } return ( {label} ); } function PositionGroupTable({ accountId, group, }: { accountId: string; group: BrokerPositionGroupConfig; }) { const [cursorStack, setCursorStack] = useState>([]); const [cursor, setCursor] = useState(undefined); const query = group.type ? { type: group.type, limit: 10, cursor } : { limit: 100, cursor }; const { data: page, isLoading, isFetching } = useBrokerPositions(accountId, query); const rawPositions = page?.items ?? []; const positions = group.type ? rawPositions : rawPositions.filter( (p) => p.instrumentType && !KNOWN_TYPES.has(p.instrumentType.toLowerCase()), ); const pageNumber = cursorStack.length + 1; const canGoBack = cursorStack.length > 0; const canGoForward = Boolean(page?.hasNext && page.nextCursor && !!group.type); function handleNext() { const nextCursor = page?.nextCursor; if (!nextCursor || !page?.hasNext || !group.type) return; setCursorStack((prev) => [...prev, cursor]); setCursor(nextCursor); } function handlePrevious() { if (cursorStack.length === 0) return; const prev = cursorStack[cursorStack.length - 1]; setCursorStack((prevStack) => prevStack.slice(0, -1)); setCursor(prev); } if (!isLoading && positions.length === 0) { return null; } return (

{group.title}

{group.type && (
{pageNumber}
)}
{isLoading && (
Тикер Название Количество Цена Стоимость
)} {!isLoading && positions.length > 0 && (
{positions.map((position) => ( ))}
Тикер Название Количество Цена Стоимость
{position.name || '-'} {formatQuantity(position.quantity)} {formatMoney(position.currentPrice)} {formatMoney(position.currentValue)}
{isFetching && (
Загрузка страницы {pageNumber}…
)}
)}
); } type BrokerPositionsSectionProps = { accountId: string; }; export function BrokerPositionsSection({ accountId }: BrokerPositionsSectionProps) { return (

Позиции

{GROUPS.map((group) => ( ))}
); }