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

173 lines
6.2 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 { Link } from 'react-router-dom';
import type { BrokerMoney, BrokerPortfolio } from '../../api/responses';
import { SkeletonBlock } from '../../components/SkeletonBlock';
import { useBrokerOperations } from '../../hooks/useBrokerOperations';
import { useBrokerAccountContext } from './BrokerAccountLayout';
import { BrokerAllocationChart } from './BrokerAllocationChart';
import { BrokerOperationsTable } from './BrokerOperationsTable';
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 formatPercent(value: number | null) {
if (value === null) return '-';
return `${new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(value)}%`;
}
function pluralize(count: number, one: string, few: string, many: string) {
const modulo100 = Math.abs(count) % 100;
const modulo10 = modulo100 % 10;
if (modulo100 > 10 && modulo100 < 20) return many;
if (modulo10 === 1) return one;
if (modulo10 >= 2 && modulo10 <= 4) return few;
return many;
}
function BrokerSummary({ portfolio }: { portfolio: BrokerPortfolio }) {
return (
<section className="broker-overview__summary" aria-label="Сводка счёта">
<div className="broker-overview__card">
<span className="broker-overview__label">Стоимость портфеля</span>
<strong className="broker-overview__total">
{formatMoney(portfolio.totals.portfolio)}
</strong>
<span>За день: {formatMoney(portfolio.yields.daily)}</span>
<span>Дневная доходность: {formatPercent(portfolio.yields.dailyPercent)}</span>
<span>Ожидаемая доходность: {formatPercent(portfolio.yields.expectedPercent)}</span>
</div>
<div className="broker-overview__card">
<span className="broker-overview__label">Денежный остаток</span>
{portfolio.cash.length === 0 ? (
<span>Нет денежных остатков</span>
) : (
<ul className="broker-overview__cash">
{portfolio.cash.map((money) => (
<li key={money.currency}>
<span>{money.currency}</span>
<strong>{formatMoney(money)}</strong>
</li>
))}
</ul>
)}
</div>
</section>
);
}
function allocationPercent(value: BrokerMoney | null, total: BrokerMoney | null) {
if (!value || !total || total.value <= 0) return 0;
return (value.value / total.value) * 100;
}
function BrokerAssetCards({
accountId,
portfolio,
}: {
accountId: string;
portfolio: BrokerPortfolio;
}) {
const basePath = `/broker/${encodeURIComponent(accountId)}`;
const cards = [
{
label: 'Акции',
count: portfolio.positionCounts.shares,
countLabel: pluralize(portfolio.positionCounts.shares, 'позиция', 'позиции', 'позиций'),
value: portfolio.totals.shares,
path: `${basePath}/shares`,
},
{
label: 'Облигации',
count: portfolio.positionCounts.bonds,
countLabel: pluralize(portfolio.positionCounts.bonds, 'выпуск', 'выпуска', 'выпусков'),
value: portfolio.totals.bonds,
path: `${basePath}/bonds`,
},
];
return (
<section className="broker-overview__assets" aria-label="Основные классы активов">
{cards.map((card) => (
<Link
className="broker-overview__card broker-overview__asset-link"
key={card.label}
to={card.path}
>
<strong className="broker-overview__asset-title">{card.label}</strong>
<span>
{card.count} {card.countLabel}
</span>
<span>{formatMoney(card.value)}</span>
<span>{allocationPercent(card.value, portfolio.totals.portfolio).toFixed(1)}%</span>
</Link>
))}
</section>
);
}
function BrokerOverviewSkeleton() {
return (
<div className="broker-overview" aria-label="Загрузка сводки счёта">
<div className="broker-overview__summary">
{[1, 2].map((item) => (
<div className="broker-overview__card" key={item}>
<SkeletonBlock height={16} width="45%" />
<SkeletonBlock height={28} width="70%" />
<SkeletonBlock height={16} width="55%" />
</div>
))}
</div>
<div className="broker-allocation">
<SkeletonBlock height={160} width={160} borderRadius={80} />
<SkeletonBlock height={80} width="60%" />
</div>
<div className="broker-overview__assets">
{[1, 2].map((item) => (
<div className="broker-overview__card" key={item}>
<SkeletonBlock height={20} width="35%" />
<SkeletonBlock height={16} width="55%" />
<SkeletonBlock height={16} width="70%" />
</div>
))}
</div>
</div>
);
}
export function BrokerAccountOverviewPage() {
const { accountId, portfolio } = useBrokerAccountContext();
const operations = useBrokerOperations(accountId, { limit: 5 });
if (portfolio.isLoading) return <BrokerOverviewSkeleton />;
if (portfolio.error || !portfolio.data) {
return <p role="alert">Не удалось загрузить сводку счёта</p>;
}
return (
<div className="broker-overview">
<BrokerSummary portfolio={portfolio.data} />
<BrokerAllocationChart portfolio={portfolio.data} />
<BrokerAssetCards accountId={accountId} portfolio={portfolio.data} />
{operations.error ? (
<p role="alert">Не удалось загрузить последние операции</p>
) : (
<BrokerOperationsTable
title="Последние операции"
headerAction={
<Link to={`/broker/${encodeURIComponent(accountId)}/operations`}>Вся история</Link>
}
emptyMessage="Операций с начала текущего года нет"
isLoading={operations.isLoading}
isFetching={operations.isFetching}
page={operations.data}
/>
)}
</div>
);
}