206 lines
6.7 KiB
TypeScript
206 lines
6.7 KiB
TypeScript
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
|
||
import type { BrokerAccountsAggregate } from '../../../entities/broker-account/model/brokerAccountsOverview';
|
||
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 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;
|
||
}
|
||
|
||
export function BrokerAccountsSummary({
|
||
aggregate,
|
||
availableCount,
|
||
totalCount,
|
||
isLoading,
|
||
}: {
|
||
aggregate: BrokerAccountsAggregate;
|
||
availableCount: number;
|
||
totalCount: number;
|
||
isLoading: boolean;
|
||
}) {
|
||
if (isLoading) {
|
||
return (
|
||
<section className="broker-accounts-summary broker-accounts-summary--loading">
|
||
<div className="broker-accounts-summary__hero">
|
||
<SkeletonBlock height={18} width="34%" />
|
||
<SkeletonBlock height={52} width="48%" />
|
||
<SkeletonBlock height={18} width="28%" />
|
||
</div>
|
||
<div className="broker-accounts-summary__metrics">
|
||
{[1, 2, 3].map((item) => (
|
||
<div className="broker-accounts-summary__metric" key={item}>
|
||
<SkeletonBlock height={14} width="45%" />
|
||
<SkeletonBlock height={26} width="70%" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<section className="broker-accounts-summary" aria-label="Общая сводка по счетам">
|
||
<div className="broker-accounts-summary__hero">
|
||
<div>
|
||
<p className="broker-accounts-summary__eyebrow">Финансовый обзор</p>
|
||
<h2 className="broker-accounts-summary__title">Счета в одном кадре</h2>
|
||
</div>
|
||
<div className="broker-accounts-summary__status">
|
||
<span>{totalCount} счетов</span>
|
||
{availableCount !== totalCount ? (
|
||
<span>
|
||
Доступно по {availableCount} из {totalCount} счетов
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="broker-accounts-summary__currency-grid">
|
||
{aggregate.portfolios.map((portfolioSummary) => {
|
||
const allocation = buildBrokerAllocation({
|
||
account: {
|
||
id: 'aggregate',
|
||
type: 'brokerage',
|
||
name: 'aggregate',
|
||
status: 'ACCOUNT_STATUS_OPEN',
|
||
openedAt: null,
|
||
accessLevel: null,
|
||
},
|
||
positionCounts: { shares: 0, bonds: 0, etf: 0, other: 0 },
|
||
totals: {
|
||
shares: {
|
||
currency: portfolioSummary.currency,
|
||
units: '0',
|
||
nano: 0,
|
||
value: portfolioSummary.allocation.shares,
|
||
},
|
||
bonds: {
|
||
currency: portfolioSummary.currency,
|
||
units: '0',
|
||
nano: 0,
|
||
value: portfolioSummary.allocation.bonds,
|
||
},
|
||
etf:
|
||
portfolioSummary.allocation.etf > 0
|
||
? {
|
||
currency: portfolioSummary.currency,
|
||
units: '0',
|
||
nano: 0,
|
||
value: portfolioSummary.allocation.etf,
|
||
}
|
||
: null,
|
||
currencies: {
|
||
currency: portfolioSummary.currency,
|
||
units: '0',
|
||
nano: 0,
|
||
value: portfolioSummary.allocation.cash,
|
||
},
|
||
futures: null,
|
||
options: null,
|
||
structuredProducts: null,
|
||
dfa: null,
|
||
portfolio: {
|
||
currency: portfolioSummary.currency,
|
||
units: '0',
|
||
nano: 0,
|
||
value: portfolioSummary.total,
|
||
},
|
||
},
|
||
yields: { expectedPercent: null, daily: null, dailyPercent: null },
|
||
cash: [],
|
||
blockedCash: [],
|
||
asOf: '',
|
||
});
|
||
|
||
return (
|
||
<article
|
||
className="broker-accounts-summary__currency-card"
|
||
key={portfolioSummary.currency}
|
||
>
|
||
<div className="broker-accounts-summary__currency-header">
|
||
<span className="broker-accounts-summary__currency">
|
||
{portfolioSummary.currency}
|
||
</span>
|
||
<strong className="broker-accounts-summary__total">
|
||
{formatBrokerCurrencyValue(portfolioSummary.currency, portfolioSummary.total)}
|
||
</strong>
|
||
</div>
|
||
<dl className="broker-accounts-summary__metrics">
|
||
<div className="broker-accounts-summary__metric">
|
||
<dt>За день</dt>
|
||
<dd>
|
||
{formatBrokerSignedCurrencyValue(
|
||
portfolioSummary.currency,
|
||
portfolioSummary.daily,
|
||
)}
|
||
</dd>
|
||
</div>
|
||
<div className="broker-accounts-summary__metric">
|
||
<dt>Динамика</dt>
|
||
<dd>{formatBrokerSignedPercent(portfolioSummary.dailyPercent)}</dd>
|
||
</div>
|
||
<div className="broker-accounts-summary__metric">
|
||
<dt>Свободные деньги</dt>
|
||
<dd>
|
||
{formatBrokerCurrencyValue(
|
||
portfolioSummary.currency,
|
||
aggregate.cash.find((cash) => cash.currency === portfolioSummary.currency)
|
||
?.value ?? 0,
|
||
)}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
<BrokerAllocationBar
|
||
title={`Распределение активов ${portfolioSummary.currency}`}
|
||
items={allocation.sectors}
|
||
/>
|
||
</article>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|