feat: migrate BrokerPositionTable, PositionTicker and TableSkeleton
This commit is contained in:
parent
9a94f4e3a9
commit
da656330b3
@ -1,4 +1,4 @@
|
||||
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
|
||||
import { Skeleton } from '@moex-vibe/design-system';
|
||||
|
||||
const tdStyle = {
|
||||
borderBottom: '1px solid #eeeeee',
|
||||
@ -15,7 +15,7 @@ export function TableSkeleton({ rows = 5, columns }: { rows?: number; columns: C
|
||||
<tr key={i}>
|
||||
{columns.map((col, j) => (
|
||||
<td key={j} style={tdStyle}>
|
||||
<SkeletonBlock height={12} width={col.width} />
|
||||
<Skeleton height={12} width={col.width} shape="text" />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@ -1,44 +1,36 @@
|
||||
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 { PositionTicker } from './PositionTicker';
|
||||
|
||||
const tableStyle = {
|
||||
const tableSx = {
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
fontSize: 14,
|
||||
} satisfies React.CSSProperties;
|
||||
} as const;
|
||||
|
||||
const thStyle = {
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
color: 'var(--color-text-secondary)',
|
||||
const thSx = {
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
color: 'text.secondary',
|
||||
fontWeight: 600,
|
||||
padding: '10px 8px',
|
||||
} satisfies React.CSSProperties;
|
||||
p: 1,
|
||||
textAlign: 'left',
|
||||
} as const;
|
||||
|
||||
const tdStyle = {
|
||||
borderBottom: '1px solid #eeeeee',
|
||||
padding: '10px 8px',
|
||||
const tdSx = {
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
p: 1,
|
||||
verticalAlign: 'top',
|
||||
} satisfies React.CSSProperties;
|
||||
textAlign: 'right',
|
||||
} as const;
|
||||
|
||||
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;
|
||||
const tdSxLeft = {
|
||||
...tdSx,
|
||||
textAlign: 'left',
|
||||
} as const;
|
||||
|
||||
function formatQuantity(value: number | null | undefined) {
|
||||
return value == null ? '-' : value.toLocaleString('ru-RU');
|
||||
@ -69,127 +61,127 @@ export function BrokerPositionTable({
|
||||
|
||||
return (
|
||||
<section aria-labelledby={`broker-${title.toLowerCase()}-heading`}>
|
||||
<div
|
||||
style={{
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
gap: 1.5,
|
||||
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}
|
||||
</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
</Heading>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
aria-label="Предыдущая страница"
|
||||
onClick={onPrevious}
|
||||
disabled={!canGoBack || isFetching}
|
||||
style={canGoBack && !isFetching ? pagButtonStyle : pagButtonDisabledStyle}
|
||||
>
|
||||
{isFetching ? (
|
||||
<span
|
||||
className="loading-spinner"
|
||||
style={{ width: 14, height: 14, display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
'←'
|
||||
)}
|
||||
</button>
|
||||
<span
|
||||
style={{
|
||||
{isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '←'}
|
||||
</Button>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: 20,
|
||||
textAlign: 'center',
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: 'text.secondary',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{pageNumber}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
</Box>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
aria-label="Следующая страница"
|
||||
onClick={onNext}
|
||||
disabled={!canGoForward || isFetching}
|
||||
style={canGoForward && !isFetching ? pagButtonStyle : pagButtonDisabledStyle}
|
||||
>
|
||||
{isFetching ? (
|
||||
<span
|
||||
className="loading-spinner"
|
||||
style={{ width: 14, height: 14, display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
'→'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '→'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isLoading ? (
|
||||
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
|
||||
<table style={tableStyle}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" style={thStyle}>
|
||||
<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">
|
||||
Тикер
|
||||
</th>
|
||||
<th align="left" style={thStyle}>
|
||||
</Box>
|
||||
<Box component="th" sx={thSx} align="left">
|
||||
Название
|
||||
</th>
|
||||
<th align="right" style={thStyle}>
|
||||
</Box>
|
||||
<Box component="th" sx={thSx} align="right">
|
||||
Количество
|
||||
</th>
|
||||
<th align="right" style={thStyle}>
|
||||
</Box>
|
||||
<Box component="th" sx={thSx} align="right">
|
||||
Цена
|
||||
</th>
|
||||
<th align="right" style={thStyle}>
|
||||
</Box>
|
||||
<Box component="th" sx={thSx} align="right">
|
||||
Стоимость
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<TableSkeleton
|
||||
rows={4}
|
||||
columns={[
|
||||
{ width: '30%' },
|
||||
{ width: '50%' },
|
||||
{ width: '20%' },
|
||||
{ width: '25%' },
|
||||
{ width: '25%' },
|
||||
]}
|
||||
/>
|
||||
</table>
|
||||
</div>
|
||||
</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 ? (
|
||||
<p style={{ color: 'var(--color-text-secondary)' }}>{emptyMessage}</p>
|
||||
<Text component="p" tone="secondary">
|
||||
{emptyMessage}
|
||||
</Text>
|
||||
) : (
|
||||
<div className="table-container">
|
||||
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
|
||||
<table aria-label={`Брокерские позиции: ${title}`} style={tableStyle}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" style={thStyle}>
|
||||
<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">
|
||||
Тикер
|
||||
</th>
|
||||
<th align="left" style={thStyle}>
|
||||
</Box>
|
||||
<Box component="th" sx={thSx} align="left">
|
||||
Название
|
||||
</th>
|
||||
<th align="right" style={thStyle}>
|
||||
</Box>
|
||||
<Box component="th" sx={thSx} align="right">
|
||||
Количество
|
||||
</th>
|
||||
<th align="right" style={thStyle}>
|
||||
</Box>
|
||||
<Box component="th" sx={thSx} align="right">
|
||||
Цена
|
||||
</th>
|
||||
<th align="right" style={thStyle}>
|
||||
</Box>
|
||||
<Box component="th" sx={thSx} align="right">
|
||||
Стоимость
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box component="tbody">
|
||||
{positions.map((position) => (
|
||||
<tr
|
||||
<Box
|
||||
component="tr"
|
||||
key={
|
||||
position.positionUid ||
|
||||
position.instrumentUid ||
|
||||
@ -197,37 +189,45 @@ export function BrokerPositionTable({
|
||||
position.figi
|
||||
}
|
||||
>
|
||||
<td style={tdStyle}>
|
||||
<Box component="td" sx={tdSxLeft}>
|
||||
<PositionTicker position={position} />
|
||||
</td>
|
||||
<td style={tdStyle}>
|
||||
<span style={{ color: 'var(--color-text-secondary)' }}>
|
||||
{position.name || '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td align="right" style={tdStyle}>
|
||||
</Box>
|
||||
<Box component="td" sx={tdSxLeft}>
|
||||
<Text tone="secondary">{position.name || '-'}</Text>
|
||||
</Box>
|
||||
<Box component="td" sx={tdSx}>
|
||||
{formatQuantity(position.quantity)}
|
||||
</td>
|
||||
<td align="right" style={tdStyle}>
|
||||
</Box>
|
||||
<Box component="td" sx={tdSx}>
|
||||
{formatMoney(position.currentPrice)}
|
||||
</td>
|
||||
<td align="right" style={tdStyle}>
|
||||
</Box>
|
||||
<Box component="td" sx={tdSx}>
|
||||
{formatMoney(position.currentValue)}
|
||||
</td>
|
||||
</tr>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{isFetching && (
|
||||
<div className="table-loading-overlay">
|
||||
<div className="loading-spinner" />
|
||||
<span style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}>
|
||||
<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}…
|
||||
</span>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Box } from '@mui/material';
|
||||
import type { BrokerPosition } from '@/shared/api/responses';
|
||||
import { getBrokerInstrumentPath } from '@/entities/broker-position';
|
||||
|
||||
@ -11,12 +12,18 @@ export function PositionTicker({ position }: { position: BrokerPosition }) {
|
||||
});
|
||||
|
||||
if (!path || label === '-') {
|
||||
return <strong>{label}</strong>;
|
||||
return (
|
||||
<Box component="span" sx={{ fontWeight: 700 }}>
|
||||
{label}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={path} style={{ fontWeight: 700 }}>
|
||||
{label}
|
||||
<Link to={path}>
|
||||
<Box component="span" sx={{ fontWeight: 700 }}>
|
||||
{label}
|
||||
</Box>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user