feat: migrate BrokerOperationsTable to design system
This commit is contained in:
parent
da656330b3
commit
e044b64de4
@ -1,6 +1,8 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { BrokerOperation, BrokerOperationsPage } from '@/shared/api/responses';
|
import type { BrokerOperation, BrokerOperationsPage } from '@/shared/api/responses';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system';
|
||||||
import { TableSkeleton } from '@/shared/ui/TableSkeleton';
|
import { TableSkeleton } from '@/shared/ui/TableSkeleton';
|
||||||
import {
|
import {
|
||||||
getBrokerOperationImpact,
|
getBrokerOperationImpact,
|
||||||
@ -10,42 +12,33 @@ import {
|
|||||||
import { getBrokerInstrumentPath } from '@/entities/broker-position';
|
import { getBrokerInstrumentPath } from '@/entities/broker-position';
|
||||||
import { formatBrokerSignedMoney } from '@/shared/lib/formatters';
|
import { formatBrokerSignedMoney } from '@/shared/lib/formatters';
|
||||||
|
|
||||||
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: 'left',
|
||||||
|
} as const;
|
||||||
|
|
||||||
const pagButtonStyle = {
|
const tdSxRight = {
|
||||||
padding: '6px 14px',
|
...tdSx,
|
||||||
borderRadius: 6,
|
textAlign: 'right',
|
||||||
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 formatDate(value: string | null) {
|
function formatDate(value: string | null) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
@ -53,11 +46,10 @@ function formatDate(value: string | null) {
|
|||||||
return new Date(value).toLocaleString('ru-RU');
|
return new Date(value).toLocaleString('ru-RU');
|
||||||
}
|
}
|
||||||
|
|
||||||
function moneyColor(impact: BrokerOperationImpact): string {
|
function operationTone(impact: BrokerOperationImpact) {
|
||||||
if (impact === 'adds') return 'var(--color-positive)';
|
if (impact === 'adds') return 'positive' as const;
|
||||||
if (impact === 'reduces') return 'var(--color-negative)';
|
if (impact === 'reduces') return 'negative' as const;
|
||||||
|
return 'primary' as const;
|
||||||
return 'var(--color-text)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function OperationInstrument({ operation }: { operation: BrokerOperation }) {
|
function OperationInstrument({ operation }: { operation: BrokerOperation }) {
|
||||||
@ -69,19 +61,23 @@ function OperationInstrument({ operation }: { operation: BrokerOperation }) {
|
|||||||
});
|
});
|
||||||
const name = operation.name || operation.description;
|
const name = operation.name || operation.description;
|
||||||
|
|
||||||
if (!path && !name) return <span>-</span>;
|
if (!path && !name) return <Text>-</Text>;
|
||||||
if (!path) return <span>{name}</span>;
|
if (!path) return <Text>{name}</Text>;
|
||||||
if (!ticker || ticker === '-') return <Link to={path}>{name}</Link>;
|
if (!ticker || ticker === '-') return <Link to={path}>{name}</Link>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid', gap: 2 }}>
|
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||||
<Link to={path} style={{ fontWeight: 700 }}>
|
<Link to={path}>
|
||||||
{ticker}
|
<Box component="span" sx={{ fontWeight: 700 }}>
|
||||||
|
{ticker}
|
||||||
|
</Box>
|
||||||
</Link>
|
</Link>
|
||||||
{name && name !== ticker && (
|
{name && name !== ticker && (
|
||||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: 12 }}>{name}</span>
|
<Text variant="caption" tone="secondary">
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,146 +99,156 @@ export function BrokerOperationsTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section aria-busy={isFetching}>
|
<section aria-busy={isFetching}>
|
||||||
<div
|
<Box
|
||||||
style={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 12,
|
gap: 1.5,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginBottom: 12,
|
mb: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 style={{ fontSize: 20, margin: 0 }}>{title}</h2>
|
<Heading level={2}>{title}</Heading>
|
||||||
{headerAction}
|
{headerAction}
|
||||||
{pagination && (
|
{pagination && (
|
||||||
<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' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'→'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
|
|
||||||
{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}>
|
||||||
Дата
|
Дата
|
||||||
</th>
|
</Box>
|
||||||
<th align="left" style={thStyle}>
|
<Box component="th" sx={thSx}>
|
||||||
Тип
|
Тип
|
||||||
</th>
|
</Box>
|
||||||
<th align="left" style={thStyle}>
|
<Box component="th" sx={thSx}>
|
||||||
Инструмент
|
Инструмент
|
||||||
</th>
|
</Box>
|
||||||
<th align="right" style={thStyle}>
|
<Box component="th" sx={{ ...thSx, textAlign: 'right' }}>
|
||||||
Сумма
|
Сумма
|
||||||
</th>
|
</Box>
|
||||||
</tr>
|
</Box>
|
||||||
</thead>
|
</Box>
|
||||||
<TableSkeleton
|
<TableSkeleton
|
||||||
rows={5}
|
rows={5}
|
||||||
columns={[{ width: '35%' }, { width: '30%' }, { width: '40%' }, { width: '25%' }]}
|
columns={[{ width: '35%' }, { width: '30%' }, { width: '40%' }, { width: '25%' }]}
|
||||||
/>
|
/>
|
||||||
</table>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
) : operations.length === 0 && !isFetching ? (
|
) : operations.length === 0 && !isFetching ? (
|
||||||
<p style={{ color: 'var(--color-text-secondary)' }}>{emptyMessage}</p>
|
<Text component="p" tone="muted">
|
||||||
|
{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 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}>
|
||||||
Дата
|
Дата
|
||||||
</th>
|
</Box>
|
||||||
<th align="left" style={thStyle}>
|
<Box component="th" sx={thSx}>
|
||||||
Тип
|
Тип
|
||||||
</th>
|
</Box>
|
||||||
<th align="left" style={thStyle}>
|
<Box component="th" sx={thSx}>
|
||||||
Инструмент
|
Инструмент
|
||||||
</th>
|
</Box>
|
||||||
<th align="right" style={thStyle}>
|
<Box component="th" sx={{ ...thSx, textAlign: 'right' }}>
|
||||||
Сумма
|
Сумма
|
||||||
</th>
|
</Box>
|
||||||
</tr>
|
</Box>
|
||||||
</thead>
|
</Box>
|
||||||
<tbody>
|
<Box component="tbody">
|
||||||
{operations.map((operation) => {
|
{operations.map((operation) => {
|
||||||
const impact = getBrokerOperationImpact(operation);
|
const impact = getBrokerOperationImpact(operation);
|
||||||
return (
|
return (
|
||||||
<tr key={operation.cursor || operation.id}>
|
<Box component="tr" key={operation.cursor || operation.id}>
|
||||||
<td style={tdStyle}>{formatDate(operation.date)}</td>
|
<Box component="td" sx={tdSx}>
|
||||||
<td style={tdStyle}>
|
{formatDate(operation.date)}
|
||||||
<span>{getBrokerOperationTypeLabel(operation)}</span>
|
</Box>
|
||||||
</td>
|
<Box component="td" sx={tdSx}>
|
||||||
<td style={tdStyle}>
|
<Text>{getBrokerOperationTypeLabel(operation)}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box component="td" sx={tdSx}>
|
||||||
<OperationInstrument operation={operation} />
|
<OperationInstrument operation={operation} />
|
||||||
</td>
|
</Box>
|
||||||
<td
|
<Box
|
||||||
|
component="td"
|
||||||
|
sx={{
|
||||||
|
...tdSxRight,
|
||||||
|
color:
|
||||||
|
operationTone(impact) === 'positive'
|
||||||
|
? 'success.main'
|
||||||
|
: operationTone(impact) === 'negative'
|
||||||
|
? 'error.main'
|
||||||
|
: 'text.primary',
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
align="right"
|
align="right"
|
||||||
style={{ ...tdStyle, color: moneyColor(impact), fontWeight: 700 }}
|
|
||||||
>
|
>
|
||||||
{formatBrokerSignedMoney(operation.payment)}
|
{formatBrokerSignedMoney(operation.payment)}
|
||||||
</td>
|
</Box>
|
||||||
</tr>
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</Box>
|
||||||
</table>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
{isFetching && (
|
{isFetching && (
|
||||||
<div className="table-loading-overlay" role="status">
|
<Box
|
||||||
<div className="loading-spinner" aria-hidden="true" />
|
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">
|
||||||
{pagination ? `Загрузка страницы ${pageNumber}…` : 'Обновление операций…'}
|
{pagination ? `Загрузка страницы ${pageNumber}…` : 'Обновление операций…'}
|
||||||
</span>
|
</Text>
|
||||||
</div>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user