codex/fsd-frontend-refactor #31
@ -1,131 +1,9 @@
|
|||||||
import { Link } from 'react-router-dom';
|
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 { useBrokerOperations } from '@/entities/broker-operation';
|
||||||
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
import { useBrokerAccountContext } from '@/widgets/broker-account-layout';
|
||||||
import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart';
|
import { BrokerAllocationChart } from '@/widgets/broker-allocation-chart';
|
||||||
import { BrokerOperationsTable } from '@/widgets/broker-operations-table';
|
import { BrokerOperationsTable } from '@/widgets/broker-operations-table';
|
||||||
import {
|
import { BrokerSummary, BrokerAssetCards, BrokerOverviewSkeleton } from '@/widgets/broker-overview';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BrokerAccountOverviewPage() {
|
export function BrokerAccountOverviewPage() {
|
||||||
const { accountId, portfolio } = useBrokerAccountContext();
|
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