152 lines
4.5 KiB
TypeScript
152 lines
4.5 KiB
TypeScript
import { Link } from 'react-router-dom';
|
||
import type { BrokerMoney, BrokerPosition } from '../../api/responses';
|
||
import { getBrokerInstrumentPath, getBrokerPositionGroup } from './brokerDisplay';
|
||
|
||
type BrokerPositionGroupConfig = {
|
||
key: 'shares' | 'bonds' | 'other';
|
||
title: string;
|
||
};
|
||
|
||
const GROUPS: BrokerPositionGroupConfig[] = [
|
||
{ key: 'shares', title: 'Акции' },
|
||
{ key: 'bonds', title: 'Облигации' },
|
||
{ key: 'other', title: 'Другие инструменты' },
|
||
];
|
||
|
||
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 '-';
|
||
|
||
return new Intl.NumberFormat('ru-RU', {
|
||
style: 'currency',
|
||
currency: value.currency || 'RUB',
|
||
maximumFractionDigits: 2,
|
||
}).format(value.value);
|
||
}
|
||
|
||
function formatQuantity(value: number | null | undefined) {
|
||
return value == null ? '-' : value.toLocaleString('ru-RU');
|
||
}
|
||
|
||
function PositionTicker({ position }: { position: BrokerPosition }) {
|
||
const label = position.ticker || position.figi || '-';
|
||
const path = getBrokerInstrumentPath({
|
||
ticker: position.ticker,
|
||
instrumentType: position.instrumentType,
|
||
classCode: position.classCode,
|
||
});
|
||
|
||
if (!path || label === '-') {
|
||
return <strong>{label}</strong>;
|
||
}
|
||
|
||
return (
|
||
<Link to={path} style={{ fontWeight: 700 }}>
|
||
{label}
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
function PositionTable({ title, positions }: { title: string; positions: BrokerPosition[] }) {
|
||
return (
|
||
<section>
|
||
<h3 style={{ fontSize: 18, marginBottom: 10 }}>{title}</h3>
|
||
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
|
||
<table aria-label={`Брокерские позиции: ${title}`} style={tableStyle}>
|
||
<thead>
|
||
<tr>
|
||
<th align="left" style={thStyle}>
|
||
Тикер
|
||
</th>
|
||
<th align="left" style={thStyle}>
|
||
Название
|
||
</th>
|
||
<th align="right" style={thStyle}>
|
||
Количество
|
||
</th>
|
||
<th align="right" style={thStyle}>
|
||
Цена
|
||
</th>
|
||
<th align="right" style={thStyle}>
|
||
Стоимость
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{positions.map((position) => (
|
||
<tr
|
||
key={
|
||
position.positionUid || position.instrumentUid || position.ticker || position.figi
|
||
}
|
||
>
|
||
<td style={tdStyle}>
|
||
<PositionTicker position={position} />
|
||
</td>
|
||
<td style={tdStyle}>
|
||
<span style={{ color: 'var(--color-text-secondary)' }}>
|
||
{position.name || '-'}
|
||
</span>
|
||
</td>
|
||
<td align="right" style={tdStyle}>
|
||
{formatQuantity(position.quantity)}
|
||
</td>
|
||
<td align="right" style={tdStyle}>
|
||
{formatMoney(position.currentPrice)}
|
||
</td>
|
||
<td align="right" style={tdStyle}>
|
||
{formatMoney(position.currentValue)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
export function BrokerPositionsSection({ positions }: { positions: BrokerPosition[] }) {
|
||
const grouped = GROUPS.map((group) => ({
|
||
...group,
|
||
positions: positions.filter((position) => getBrokerPositionGroup(position) === group.key),
|
||
})).filter((group) => group.positions.length > 0);
|
||
|
||
if (grouped.length === 0) {
|
||
return (
|
||
<section>
|
||
<h2 style={{ fontSize: 20, marginBottom: 12 }}>Позиции</h2>
|
||
<p style={{ color: 'var(--color-text-secondary)' }}>В портфеле нет позиций</p>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<h2 style={{ fontSize: 20, marginBottom: 12 }}>Позиции</h2>
|
||
<div style={{ display: 'grid', gap: 20 }}>
|
||
{grouped.map((group) => (
|
||
<PositionTable key={group.key} title={group.title} positions={group.positions} />
|
||
))}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|