140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
import { useState } from 'react';
|
||
import { useParams } from 'react-router-dom';
|
||
import type { BrokerMoney } from '../../api/responses';
|
||
import { useBrokerOperations } from '../../hooks/useBrokerOperations';
|
||
import { useBrokerPortfolio } from '../../hooks/useBrokerPortfolio';
|
||
import { BrokerOperationsTable } from './BrokerOperationsTable';
|
||
import { BrokerPositionsSection } from './BrokerPositionsSection';
|
||
import { SkeletonBlock } from '../../components/SkeletonBlock';
|
||
|
||
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);
|
||
}
|
||
|
||
export function BrokerAccountDetailPage() {
|
||
const { accountId } = useParams();
|
||
const [operationCursor, setOperationCursor] = useState<string | undefined>(undefined);
|
||
const [operationCursorStack, setOperationCursorStack] = useState<Array<string | undefined>>([]);
|
||
const portfolio = useBrokerPortfolio(accountId);
|
||
const operations = useBrokerOperations(accountId, { limit: 10, cursor: operationCursor });
|
||
|
||
if (portfolio.isLoading) {
|
||
return (
|
||
<div style={{ display: 'grid', gap: 24 }}>
|
||
<div style={{ display: 'grid', gap: 12 }}>
|
||
<SkeletonBlock height={32} width="60%" />
|
||
<SkeletonBlock height={24} width="40%" />
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
{[1, 2, 3].map((i) => (
|
||
<div
|
||
key={i}
|
||
style={{
|
||
padding: 16,
|
||
background: 'var(--color-surface)',
|
||
border: '1px solid #e0e0e0',
|
||
borderRadius: 8,
|
||
}}
|
||
>
|
||
<SkeletonBlock height={14} width="40%" />
|
||
<div style={{ height: 8 }} />
|
||
<SkeletonBlock height={20} width="60%" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (portfolio.error || !portfolio.data) {
|
||
return <p style={{ color: 'var(--color-negative)' }}>Не удалось загрузить портфель</p>;
|
||
}
|
||
|
||
function handleNextOperationsPage() {
|
||
const nextCursor = operations.data?.nextCursor;
|
||
if (!nextCursor || !operations.data?.hasNext) return;
|
||
setOperationCursorStack((previous) => [...previous, operationCursor]);
|
||
setOperationCursor(nextCursor);
|
||
}
|
||
|
||
function handlePreviousOperationsPage() {
|
||
if (operationCursorStack.length === 0) return;
|
||
const nextStack = operationCursorStack.slice(0, -1);
|
||
const previousCursor = operationCursorStack[operationCursorStack.length - 1];
|
||
setOperationCursorStack(nextStack);
|
||
setOperationCursor(previousCursor);
|
||
}
|
||
|
||
return (
|
||
<div style={{ display: 'grid', gap: 24 }}>
|
||
<header>
|
||
<h1 style={{ fontSize: 28, lineHeight: 1.2, marginBottom: 12 }}>
|
||
{portfolio.data.account.name}
|
||
</h1>
|
||
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'baseline' }}>
|
||
<strong style={{ fontSize: 24 }}>{formatMoney(portfolio.data.totals.portfolio)}</strong>
|
||
<span style={{ color: 'var(--color-text-secondary)' }}>
|
||
День: {formatMoney(portfolio.data.yields.daily)}
|
||
</span>
|
||
<span style={{ color: 'var(--color-text-secondary)' }}>
|
||
Ожидаемая: {portfolio.data.yields.expectedPercent ?? '-'}%
|
||
</span>
|
||
</div>
|
||
</header>
|
||
|
||
<section
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
{portfolio.data.cash.map((money) => (
|
||
<div
|
||
key={money.currency}
|
||
style={{
|
||
background: 'var(--color-surface)',
|
||
border: '1px solid #e0e0e0',
|
||
borderRadius: 8,
|
||
padding: 16,
|
||
}}
|
||
>
|
||
<div style={{ color: 'var(--color-text-secondary)', fontSize: 13 }}>
|
||
{money.currency}
|
||
</div>
|
||
<strong>{formatMoney(money)}</strong>
|
||
</div>
|
||
))}
|
||
</section>
|
||
|
||
<BrokerPositionsSection accountId={accountId!} />
|
||
|
||
<BrokerOperationsTable
|
||
title="Операции"
|
||
emptyMessage="Операций за выбранный период нет"
|
||
isLoading={operations.isLoading}
|
||
isFetching={operations.isFetching}
|
||
page={operations.data}
|
||
pagination={{
|
||
pageNumber: operationCursorStack.length + 1,
|
||
canGoBack: operationCursorStack.length > 0,
|
||
canGoForward: Boolean(operations.data?.hasNext && operations.data.nextCursor),
|
||
onPrevious: handlePreviousOperationsPage,
|
||
onNext: handleNextOperationsPage,
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|