moex-vibe/apps/frontend/src/pages/broker/BrokerAccountDetailPage.tsx

140 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}