235 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { BrokerPositionsPage as BrokerPositionsPageData } from '@/shared/api/responses';
import { Box } from '@mui/material';
import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system';
import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters';
import { PositionTicker } from './PositionTicker';
const tableSx = {
width: '100%',
borderCollapse: 'collapse',
fontSize: 14,
} as const;
const thSx = {
borderBottom: '1px solid',
borderColor: 'divider',
color: 'text.secondary',
fontWeight: 600,
p: 1,
textAlign: 'left',
} as const;
const tdSx = {
borderBottom: '1px solid',
borderColor: 'divider',
p: 1,
verticalAlign: 'top',
textAlign: 'right',
} as const;
const tdSxLeft = {
...tdSx,
textAlign: 'left',
} as const;
function formatQuantity(value: number | null | undefined) {
return value == null ? '-' : value.toLocaleString('ru-RU');
}
export function BrokerPositionTable({
title,
page,
isLoading,
isFetching,
emptyMessage,
pageNumber,
onNext,
onPrevious,
}: {
title: string;
page: BrokerPositionsPageData | undefined;
isLoading: boolean;
isFetching: boolean;
emptyMessage: string;
pageNumber: number;
onNext: () => void;
onPrevious: () => void;
}) {
const positions = page?.items ?? [];
const canGoBack = pageNumber > 1;
const canGoForward = Boolean(page?.hasNext && page.nextCursor);
return (
<section aria-labelledby={`broker-${title.toLowerCase()}-heading`}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
justifyContent: 'space-between',
mb: 1.5,
}}
>
<Heading level={2} id={`broker-${title.toLowerCase()}-heading`}>
{title}
</Heading>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="secondary"
size="small"
aria-label="Предыдущая страница"
onClick={onPrevious}
disabled={!canGoBack || isFetching}
>
{isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '←'}
</Button>
<Box
sx={{
minWidth: 20,
textAlign: 'center',
color: 'text.secondary',
fontSize: 14,
fontWeight: 600,
}}
>
{pageNumber}
</Box>
<Button
variant="secondary"
size="small"
aria-label="Следующая страница"
onClick={onNext}
disabled={!canGoForward || isFetching}
>
{isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '→'}
</Button>
</Box>
</Box>
{isLoading ? (
<Box sx={{ overflowX: 'auto', bgcolor: 'surface.default' }}>
<Box component="table" sx={tableSx}>
<Box component="thead">
<Box component="tr">
<Box component="th" sx={thSx} align="left">
Тикер
</Box>
<Box component="th" sx={thSx} align="left">
Название
</Box>
<Box component="th" sx={thSx} align="right">
Количество
</Box>
<Box component="th" sx={thSx} align="right">
Цена
</Box>
<Box component="th" sx={thSx} align="right">
Стоимость
</Box>
</Box>
</Box>
<Box component="tbody">
{[1, 2, 3, 4].map((row) => (
<Box component="tr" key={row}>
<Box component="td" sx={tdSxLeft}>
<Skeleton height={12} width="30%" shape="text" />
</Box>
<Box component="td" sx={tdSxLeft}>
<Skeleton height={12} width="50%" shape="text" />
</Box>
<Box component="td" sx={tdSx}>
<Skeleton height={12} width="20%" shape="text" />
</Box>
<Box component="td" sx={tdSx}>
<Skeleton height={12} width="25%" shape="text" />
</Box>
<Box component="td" sx={tdSx}>
<Skeleton height={12} width="25%" shape="text" />
</Box>
</Box>
))}
</Box>
</Box>
</Box>
) : positions.length === 0 && !isFetching ? (
<Text component="p" tone="secondary">
{emptyMessage}
</Text>
) : (
<Box sx={{ position: 'relative' }}>
<Box sx={{ overflowX: 'auto', bgcolor: 'surface.default' }}>
<Box component="table" sx={tableSx} aria-label={`Брокерские позиции: ${title}`}>
<Box component="thead">
<Box component="tr">
<Box component="th" sx={thSx} align="left">
Тикер
</Box>
<Box component="th" sx={thSx} align="left">
Название
</Box>
<Box component="th" sx={thSx} align="right">
Количество
</Box>
<Box component="th" sx={thSx} align="right">
Цена
</Box>
<Box component="th" sx={thSx} align="right">
Стоимость
</Box>
</Box>
</Box>
<Box component="tbody">
{positions.map((position) => (
<Box
component="tr"
key={
position.positionUid ||
position.instrumentUid ||
position.ticker ||
position.figi
}
>
<Box component="td" sx={tdSxLeft}>
<PositionTicker position={position} />
</Box>
<Box component="td" sx={tdSxLeft}>
<Text tone="secondary">{position.name || '-'}</Text>
</Box>
<Box component="td" sx={tdSx}>
{formatQuantity(position.quantity)}
</Box>
<Box component="td" sx={tdSx}>
{formatMoney(position.currentPrice)}
</Box>
<Box component="td" sx={tdSx}>
{formatMoney(position.currentValue)}
</Box>
</Box>
))}
</Box>
</Box>
</Box>
{isFetching && (
<Box
sx={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 1,
bgcolor: 'rgba(255,255,255,0.7)',
}}
>
<Box className="loading-spinner" />
<Text variant="caption" tone="secondary">
Загрузка страницы {pageNumber}
</Text>
</Box>
)}
</Box>
)}
</section>
);
}