233 lines
6.6 KiB
TypeScript
233 lines
6.6 KiB
TypeScript
import { Link } from 'react-router-dom';
|
||
import type { BrokerMoney, BrokerOperation, BrokerOperationsPage } from '../../api/responses';
|
||
import {
|
||
getBrokerInstrumentPath,
|
||
getBrokerOperationImpact,
|
||
getBrokerOperationTypeLabel,
|
||
type BrokerOperationImpact,
|
||
} from './brokerDisplay';
|
||
import { TableSkeleton } from '../../components/TableSkeleton';
|
||
|
||
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;
|
||
|
||
function formatMoney(value: BrokerMoney | null | undefined) {
|
||
if (!value) return '-';
|
||
const formatted = new Intl.NumberFormat('ru-RU', {
|
||
style: 'currency',
|
||
currency: value.currency || 'RUB',
|
||
maximumFractionDigits: 2,
|
||
}).format(value.value);
|
||
return value.value > 0 ? `+${formatted}` : formatted;
|
||
}
|
||
|
||
function formatDate(value: string | null) {
|
||
if (!value) return '-';
|
||
|
||
return new Date(value).toLocaleString('ru-RU');
|
||
}
|
||
|
||
function moneyColor(impact: BrokerOperationImpact): string {
|
||
if (impact === 'adds') return 'var(--color-positive)';
|
||
if (impact === 'reduces') return 'var(--color-negative)';
|
||
|
||
return 'var(--color-text)';
|
||
}
|
||
|
||
function OperationInstrument({ operation }: { operation: BrokerOperation }) {
|
||
const ticker = operation.ticker || operation.description || '-';
|
||
const path = getBrokerInstrumentPath({
|
||
ticker: operation.ticker,
|
||
instrumentType: operation.instrumentType,
|
||
classCode: operation.classCode,
|
||
});
|
||
const name = operation.name || operation.description;
|
||
|
||
if (!path && !name) return <span>-</span>;
|
||
if (!path) return <span>{name}</span>;
|
||
if (!ticker || ticker === '-') return <Link to={path}>{name}</Link>;
|
||
|
||
return (
|
||
<div style={{ display: 'grid', gap: 2 }}>
|
||
<Link to={path} style={{ fontWeight: 700 }}>
|
||
{ticker}
|
||
</Link>
|
||
{name && name !== ticker && (
|
||
<span style={{ color: 'var(--color-text-secondary)', fontSize: 12 }}>{name}</span>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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;
|
||
|
||
export function BrokerOperationsTable({
|
||
isLoading,
|
||
page,
|
||
pageNumber,
|
||
canGoBack,
|
||
canGoForward,
|
||
onPrevious,
|
||
onNext,
|
||
}: {
|
||
isLoading: boolean;
|
||
page: BrokerOperationsPage | undefined;
|
||
pageNumber: number;
|
||
canGoBack: boolean;
|
||
canGoForward: boolean;
|
||
onPrevious: () => void;
|
||
onNext: () => void;
|
||
}) {
|
||
const operations = page?.items ?? [];
|
||
|
||
return (
|
||
<section>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
justifyContent: 'space-between',
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
<h2 style={{ fontSize: 20, margin: 0 }}>Операции</h2>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<button
|
||
type="button"
|
||
onClick={onPrevious}
|
||
disabled={!canGoBack}
|
||
style={canGoBack ? pagButtonStyle : pagButtonDisabledStyle}
|
||
>
|
||
←
|
||
</button>
|
||
<span
|
||
style={{
|
||
minWidth: 20,
|
||
textAlign: 'center',
|
||
color: 'var(--color-text-secondary)',
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
}}
|
||
>
|
||
{pageNumber}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={onNext}
|
||
disabled={!canGoForward}
|
||
style={canGoForward ? pagButtonStyle : pagButtonDisabledStyle}
|
||
>
|
||
→
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{isLoading ? (
|
||
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
|
||
<table style={tableStyle}>
|
||
<thead>
|
||
<tr>
|
||
<th align="left" style={thStyle}>
|
||
Дата
|
||
</th>
|
||
<th align="left" style={thStyle}>
|
||
Тип
|
||
</th>
|
||
<th align="left" style={thStyle}>
|
||
Инструмент
|
||
</th>
|
||
<th align="right" style={thStyle}>
|
||
Сумма
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<TableSkeleton
|
||
rows={5}
|
||
columns={[{ width: '35%' }, { width: '30%' }, { width: '40%' }, { width: '25%' }]}
|
||
/>
|
||
</table>
|
||
</div>
|
||
) : operations.length === 0 ? (
|
||
<p style={{ color: 'var(--color-text-secondary)' }}>Операций за выбранный период нет</p>
|
||
) : (
|
||
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
|
||
<table style={tableStyle}>
|
||
<thead>
|
||
<tr>
|
||
<th align="left" style={thStyle}>
|
||
Дата
|
||
</th>
|
||
<th align="left" style={thStyle}>
|
||
Тип
|
||
</th>
|
||
<th align="left" style={thStyle}>
|
||
Инструмент
|
||
</th>
|
||
<th align="right" style={thStyle}>
|
||
Сумма
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{operations.map((operation) => {
|
||
const impact = getBrokerOperationImpact(operation);
|
||
|
||
return (
|
||
<tr key={operation.cursor || operation.id}>
|
||
<td style={tdStyle}>{formatDate(operation.date)}</td>
|
||
<td style={tdStyle}>
|
||
<span>{getBrokerOperationTypeLabel(operation)}</span>
|
||
</td>
|
||
<td style={tdStyle}>
|
||
<OperationInstrument operation={operation} />
|
||
</td>
|
||
<td
|
||
align="right"
|
||
style={{ ...tdStyle, color: moneyColor(impact), fontWeight: 700 }}
|
||
>
|
||
{formatMoney(operation.payment)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|