codex/broker-accounts-page #34

Merged
ksv741 merged 19 commits from codex/broker-accounts-page into main 2026-06-21 20:57:47 +03:00
3 changed files with 149 additions and 142 deletions
Showing only changes of commit da656330b3 - Show all commits

View File

@ -1,4 +1,4 @@
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock'; import { Skeleton } from '@moex-vibe/design-system';
const tdStyle = { const tdStyle = {
borderBottom: '1px solid #eeeeee', borderBottom: '1px solid #eeeeee',
@ -15,7 +15,7 @@ export function TableSkeleton({ rows = 5, columns }: { rows?: number; columns: C
<tr key={i}> <tr key={i}>
{columns.map((col, j) => ( {columns.map((col, j) => (
<td key={j} style={tdStyle}> <td key={j} style={tdStyle}>
<SkeletonBlock height={12} width={col.width} /> <Skeleton height={12} width={col.width} shape="text" />
</td> </td>
))} ))}
</tr> </tr>

View File

@ -1,44 +1,36 @@
import type { BrokerPositionsPage as BrokerPositionsPageData } from '@/shared/api/responses'; import type { BrokerPositionsPage as BrokerPositionsPageData } from '@/shared/api/responses';
import { TableSkeleton } from '@/shared/ui/TableSkeleton'; import { Box } from '@mui/material';
import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system';
import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters'; import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters';
import { PositionTicker } from './PositionTicker'; import { PositionTicker } from './PositionTicker';
const tableStyle = { const tableSx = {
width: '100%', width: '100%',
borderCollapse: 'collapse', borderCollapse: 'collapse',
fontSize: 14, fontSize: 14,
} satisfies React.CSSProperties; } as const;
const thStyle = { const thSx = {
borderBottom: '1px solid #e0e0e0', borderBottom: '1px solid',
color: 'var(--color-text-secondary)', borderColor: 'divider',
color: 'text.secondary',
fontWeight: 600, fontWeight: 600,
padding: '10px 8px', p: 1,
} satisfies React.CSSProperties; textAlign: 'left',
} as const;
const tdStyle = { const tdSx = {
borderBottom: '1px solid #eeeeee', borderBottom: '1px solid',
padding: '10px 8px', borderColor: 'divider',
p: 1,
verticalAlign: 'top', verticalAlign: 'top',
} satisfies React.CSSProperties; textAlign: 'right',
} as const;
const pagButtonStyle = { const tdSxLeft = {
padding: '6px 14px', ...tdSx,
borderRadius: 6, textAlign: 'left',
border: '1px solid #e0e0e0', } as const;
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 formatQuantity(value: number | null | undefined) { function formatQuantity(value: number | null | undefined) {
return value == null ? '-' : value.toLocaleString('ru-RU'); return value == null ? '-' : value.toLocaleString('ru-RU');
@ -69,127 +61,127 @@ export function BrokerPositionTable({
return ( return (
<section aria-labelledby={`broker-${title.toLowerCase()}-heading`}> <section aria-labelledby={`broker-${title.toLowerCase()}-heading`}>
<div <Box
style={{ sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: 1.5,
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: 10, mb: 1.5,
}} }}
> >
<h2 id={`broker-${title.toLowerCase()}-heading`} style={{ fontSize: 20, margin: 0 }}> <Heading level={2} id={`broker-${title.toLowerCase()}-heading`}>
{title} {title}
</h2> </Heading>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<button <Button
type="button" variant="secondary"
size="small"
aria-label="Предыдущая страница" aria-label="Предыдущая страница"
onClick={onPrevious} onClick={onPrevious}
disabled={!canGoBack || isFetching} disabled={!canGoBack || isFetching}
style={canGoBack && !isFetching ? pagButtonStyle : pagButtonDisabledStyle}
> >
{isFetching ? ( {isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '←'}
<span </Button>
className="loading-spinner" <Box
style={{ width: 14, height: 14, display: 'block' }} sx={{
/>
) : (
'←'
)}
</button>
<span
style={{
minWidth: 20, minWidth: 20,
textAlign: 'center', textAlign: 'center',
color: 'var(--color-text-secondary)', color: 'text.secondary',
fontSize: 14, fontSize: 14,
fontWeight: 600, fontWeight: 600,
}} }}
> >
{pageNumber} {pageNumber}
</span> </Box>
<button <Button
type="button" variant="secondary"
size="small"
aria-label="Следующая страница" aria-label="Следующая страница"
onClick={onNext} onClick={onNext}
disabled={!canGoForward || isFetching} disabled={!canGoForward || isFetching}
style={canGoForward && !isFetching ? pagButtonStyle : pagButtonDisabledStyle}
> >
{isFetching ? ( {isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '→'}
<span </Button>
className="loading-spinner" </Box>
style={{ width: 14, height: 14, display: 'block' }} </Box>
/>
) : (
'→'
)}
</button>
</div>
</div>
{isLoading ? ( {isLoading ? (
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}> <Box sx={{ overflowX: 'auto', bgcolor: 'surface.default' }}>
<table style={tableStyle}> <Box component="table" sx={tableSx}>
<thead> <Box component="thead">
<tr> <Box component="tr">
<th align="left" style={thStyle}> <Box component="th" sx={thSx} align="left">
Тикер Тикер
</th> </Box>
<th align="left" style={thStyle}> <Box component="th" sx={thSx} align="left">
Название Название
</th> </Box>
<th align="right" style={thStyle}> <Box component="th" sx={thSx} align="right">
Количество Количество
</th> </Box>
<th align="right" style={thStyle}> <Box component="th" sx={thSx} align="right">
Цена Цена
</th> </Box>
<th align="right" style={thStyle}> <Box component="th" sx={thSx} align="right">
Стоимость Стоимость
</th> </Box>
</tr> </Box>
</thead> </Box>
<TableSkeleton <Box component="tbody">
rows={4} {[1, 2, 3, 4].map((row) => (
columns={[ <Box component="tr" key={row}>
{ width: '30%' }, <Box component="td" sx={tdSxLeft}>
{ width: '50%' }, <Skeleton height={12} width="30%" shape="text" />
{ width: '20%' }, </Box>
{ width: '25%' }, <Box component="td" sx={tdSxLeft}>
{ width: '25%' }, <Skeleton height={12} width="50%" shape="text" />
]} </Box>
/> <Box component="td" sx={tdSx}>
</table> <Skeleton height={12} width="20%" shape="text" />
</div> </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 ? ( ) : positions.length === 0 && !isFetching ? (
<p style={{ color: 'var(--color-text-secondary)' }}>{emptyMessage}</p> <Text component="p" tone="secondary">
{emptyMessage}
</Text>
) : ( ) : (
<div className="table-container"> <Box sx={{ position: 'relative' }}>
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}> <Box sx={{ overflowX: 'auto', bgcolor: 'surface.default' }}>
<table aria-label={`Брокерские позиции: ${title}`} style={tableStyle}> <Box component="table" sx={tableSx} aria-label={`Брокерские позиции: ${title}`}>
<thead> <Box component="thead">
<tr> <Box component="tr">
<th align="left" style={thStyle}> <Box component="th" sx={thSx} align="left">
Тикер Тикер
</th> </Box>
<th align="left" style={thStyle}> <Box component="th" sx={thSx} align="left">
Название Название
</th> </Box>
<th align="right" style={thStyle}> <Box component="th" sx={thSx} align="right">
Количество Количество
</th> </Box>
<th align="right" style={thStyle}> <Box component="th" sx={thSx} align="right">
Цена Цена
</th> </Box>
<th align="right" style={thStyle}> <Box component="th" sx={thSx} align="right">
Стоимость Стоимость
</th> </Box>
</tr> </Box>
</thead> </Box>
<tbody> <Box component="tbody">
{positions.map((position) => ( {positions.map((position) => (
<tr <Box
component="tr"
key={ key={
position.positionUid || position.positionUid ||
position.instrumentUid || position.instrumentUid ||
@ -197,37 +189,45 @@ export function BrokerPositionTable({
position.figi position.figi
} }
> >
<td style={tdStyle}> <Box component="td" sx={tdSxLeft}>
<PositionTicker position={position} /> <PositionTicker position={position} />
</td> </Box>
<td style={tdStyle}> <Box component="td" sx={tdSxLeft}>
<span style={{ color: 'var(--color-text-secondary)' }}> <Text tone="secondary">{position.name || '-'}</Text>
{position.name || '-'} </Box>
</span> <Box component="td" sx={tdSx}>
</td>
<td align="right" style={tdStyle}>
{formatQuantity(position.quantity)} {formatQuantity(position.quantity)}
</td> </Box>
<td align="right" style={tdStyle}> <Box component="td" sx={tdSx}>
{formatMoney(position.currentPrice)} {formatMoney(position.currentPrice)}
</td> </Box>
<td align="right" style={tdStyle}> <Box component="td" sx={tdSx}>
{formatMoney(position.currentValue)} {formatMoney(position.currentValue)}
</td> </Box>
</tr> </Box>
))} ))}
</tbody> </Box>
</table> </Box>
</div> </Box>
{isFetching && ( {isFetching && (
<div className="table-loading-overlay"> <Box
<div className="loading-spinner" /> sx={{
<span style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}> 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} Загрузка страницы {pageNumber}
</span> </Text>
</div> </Box>
)} )}
</div> </Box>
)} )}
</section> </section>
); );

View File

@ -1,4 +1,5 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Box } from '@mui/material';
import type { BrokerPosition } from '@/shared/api/responses'; import type { BrokerPosition } from '@/shared/api/responses';
import { getBrokerInstrumentPath } from '@/entities/broker-position'; import { getBrokerInstrumentPath } from '@/entities/broker-position';
@ -11,12 +12,18 @@ export function PositionTicker({ position }: { position: BrokerPosition }) {
}); });
if (!path || label === '-') { if (!path || label === '-') {
return <strong>{label}</strong>; return (
<Box component="span" sx={{ fontWeight: 700 }}>
{label}
</Box>
);
} }
return ( return (
<Link to={path} style={{ fontWeight: 700 }}> <Link to={path}>
<Box component="span" sx={{ fontWeight: 700 }}>
{label} {label}
</Box>
</Link> </Link>
); );
} }