202 lines
5.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 { Link } from 'react-router-dom';
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
import type { BrokerAccount, BrokerMoney, BrokerPortfolio } from '@/shared/api/responses';
import { buildBrokerAllocation } from '@/entities/broker-position';
import { BrokerAllocationBar } from '@/widgets/broker-allocation-chart';
function formatBrokerCurrencyValue(currency: string, value: number): string {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: currency || 'RUB',
maximumFractionDigits: 2,
}).format(value);
}
function formatBrokerMoney(value: BrokerMoney | null | undefined): string {
if (!value) {
return '—';
}
return formatBrokerCurrencyValue(value.currency, value.value);
}
function formatBrokerSignedCurrencyValue(currency: string, value: number | null): string {
if (value === null) {
return '—';
}
const formatted = formatBrokerCurrencyValue(currency, Math.abs(value));
if (value > 0) {
return `+${formatted}`;
}
if (value < 0) {
return `${formatted}`;
}
return formatted;
}
function formatBrokerSignedPercent(value: number | null): string {
if (value === null) {
return '—';
}
const formatted = `${new Intl.NumberFormat('ru-RU', {
maximumFractionDigits: 2,
}).format(Math.abs(value))}%`;
if (value > 0) {
return `+${formatted}`;
}
if (value < 0) {
return `${formatted}`;
}
return formatted;
}
function formatBrokerDate(value: string | null | undefined): string | null {
if (!value) {
return null;
}
return new Date(value).toLocaleDateString('ru-RU');
}
function brokerAccountTypeLabel(type: 'brokerage' | 'iis'): string {
return type === 'iis' ? 'ИИС' : 'Брокерский счёт';
}
function BrokerAccountCardSkeleton({ name, typeLabel }: { name: string; typeLabel: string }) {
return (
<article className="broker-account-card broker-account-card--loading" aria-busy="true">
<div className="broker-account-card__header">
<div>
<p className="broker-account-card__eyebrow">{typeLabel}</p>
<h2 className="broker-account-card__title">{name}</h2>
</div>
</div>
<div className="broker-account-card__grid">
{[1, 2, 3, 4].map((item) => (
<div className="broker-account-card__stat" key={item}>
<SkeletonBlock height={12} width="50%" />
<SkeletonBlock height={24} width="75%" />
</div>
))}
</div>
<SkeletonBlock height={16} width="100%" borderRadius={999} />
<SkeletonBlock height={16} width="65%" />
</article>
);
}
function BrokerAccountCardError({
account,
onRetry,
}: {
account: BrokerAccount;
onRetry: () => void;
}) {
return (
<article className="broker-account-card broker-account-card--error">
<div className="broker-account-card__header">
<div>
<p className="broker-account-card__eyebrow">{brokerAccountTypeLabel(account.type)}</p>
<h2 className="broker-account-card__title">{account.name}</h2>
</div>
</div>
<div className="broker-account-card__alert" role="alert">
<p>Не удалось загрузить данные счёта</p>
<button className="broker-account-card__retry" type="button" onClick={onRetry}>
Повторить
</button>
</div>
</article>
);
}
function BrokerAccountCardSuccess({
account,
portfolio,
}: {
account: BrokerAccount;
portfolio: BrokerPortfolio;
}) {
const typeLabel = brokerAccountTypeLabel(account.type);
const openedAt = formatBrokerDate(account.openedAt);
const allocation = buildBrokerAllocation(portfolio);
return (
<Link
className="broker-account-card broker-account-card--link"
to={`/broker/${encodeURIComponent(account.id)}`}
>
<div className="broker-account-card__header">
<div>
<p className="broker-account-card__eyebrow">{typeLabel}</p>
<h2 className="broker-account-card__title">{account.name}</h2>
</div>
{openedAt ? <p className="broker-account-card__opened">Открыт {openedAt}</p> : null}
</div>
<div className="broker-account-card__grid">
<div className="broker-account-card__stat broker-account-card__stat--wide">
<span>Стоимость</span>
<strong>{formatBrokerMoney(portfolio.totals.portfolio)}</strong>
</div>
<div className="broker-account-card__stat">
<span>За день</span>
<strong>
{formatBrokerSignedCurrencyValue(
portfolio.totals.portfolio?.currency ?? 'RUB',
portfolio.yields.daily?.value ?? null,
)}
</strong>
</div>
<div className="broker-account-card__stat">
<span>Дневная динамика</span>
<strong>{formatBrokerSignedPercent(portfolio.yields.dailyPercent)}</strong>
</div>
<div className="broker-account-card__stat">
<span>Ожидаемая доходность</span>
<strong>{formatBrokerSignedPercent(portfolio.yields.expectedPercent)}</strong>
</div>
</div>
<BrokerAllocationBar title={`Структура счёта ${account.name}`} items={allocation.sectors} />
</Link>
);
}
export function BrokerAccountCard({
account,
portfolio,
isLoading,
error,
onRetry,
}: {
account: BrokerAccount;
portfolio?: BrokerPortfolio;
isLoading: boolean;
error: Error | null;
onRetry: () => void;
}) {
if (isLoading && !portfolio) {
return (
<BrokerAccountCardSkeleton
name={account.name}
typeLabel={brokerAccountTypeLabel(account.type)}
/>
);
}
if (error || !portfolio) {
return <BrokerAccountCardError account={account} onRetry={onRetry} />;
}
return <BrokerAccountCardSuccess account={account} portfolio={portfolio} />;
}