codex/fsd-frontend-refactor #31
@ -1,131 +1,9 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { BrokerMoney, BrokerPortfolio } from '@/shared/api/responses';
|
||||
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
|
||||
import { useBrokerOperations } from '@/entities/broker-operation';
|
||||
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
||||
import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart';
|
||||
import { BrokerOperationsTable } from '@/widgets/broker-operations-table';
|
||||
import {
|
||||
formatBrokerMoney as formatMoney,
|
||||
formatBrokerPercent as formatPercent,
|
||||
pluralize,
|
||||
} from '@/shared/lib/formatters';
|
||||
|
||||
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, index) => (
|
||||
<li key={`${money.currency}-${index}`}>
|
||||
<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 null;
|
||||
return (value.value / total.value) * 100;
|
||||
}
|
||||
|
||||
function formatAllocationPercent(value: number | null) {
|
||||
return value === null ? '—' : `${value.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
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>
|
||||
{formatAllocationPercent(allocationPercent(card.value, portfolio.totals.portfolio))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
import { BrokerSummary, BrokerAssetCards, BrokerOverviewSkeleton } from '@/widgets/broker-overview';
|
||||
|
||||
export function BrokerAccountOverviewPage() {
|
||||
const { accountId, portfolio } = useBrokerAccountContext();
|
||||
|
||||
3
apps/frontend/src/widgets/broker-overview/index.ts
Normal file
3
apps/frontend/src/widgets/broker-overview/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { BrokerSummary } from './ui/BrokerSummary';
|
||||
export { BrokerAssetCards } from './ui/BrokerAssetCards';
|
||||
export { BrokerOverviewSkeleton } from './ui/BrokerOverviewSkeleton';
|
||||
@ -0,0 +1,59 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { BrokerMoney, BrokerPortfolio } from '@/shared/api/responses';
|
||||
import { formatBrokerMoney, pluralize } from '@/shared/lib/formatters';
|
||||
|
||||
function allocationPercent(value: BrokerMoney | null, total: BrokerMoney | null) {
|
||||
if (!value || !total || total.value <= 0) return null;
|
||||
return (value.value / total.value) * 100;
|
||||
}
|
||||
|
||||
function formatAllocationPercent(value: number | null) {
|
||||
return value === null ? '\u2014' : `${value.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
export 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>{formatBrokerMoney(card.value)}</span>
|
||||
<span>
|
||||
{formatAllocationPercent(allocationPercent(card.value, portfolio.totals.portfolio))}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
|
||||
|
||||
export 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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import type { BrokerPortfolio } from '@/shared/api/responses';
|
||||
import {
|
||||
formatBrokerMoney as formatMoney,
|
||||
formatBrokerPercent as formatPercent,
|
||||
} from '@/shared/lib/formatters';
|
||||
|
||||
export 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, index) => (
|
||||
<li key={`${money.currency}-${index}`}>
|
||||
<span>{money.currency}</span>
|
||||
<strong>{formatMoney(money)}</strong>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user