feat: add bond pages and portfolio navigation

This commit is contained in:
2026-09-10 22:30:47 +03:00
parent 561222e468
commit 232d7f9990
39 changed files with 4217 additions and 221 deletions
+18 -8
View File
@@ -121,15 +121,22 @@ function emptyProps() {
}
describe("AccountPage states", () => {
it.each(["account-1", "all"])(
"uses verified ticker hrefs for both name and logo in %s",
async (accountId) => {
it.each(
["account-1", "all"].flatMap((accountId) =>
["STOCK", "BOND"].map((type) => ({ accountId, type })),
),
)(
"links name and logo to the exact issue in $accountId for $type",
async ({ accountId, type }) => {
const collection = type === "STOCK" ? "stocks" : "bonds";
const slug = type === "STOCK" ? "SBERP" : "SU26238RMFS4";
const uid = "c190ff1f-1447-4227-b543-316332699ca5";
const requests = vi.fn();
const position = {
...portfolio.positions[0],
type,
instrumentUid: uid,
ticker: "SBERP",
ticker: slug,
displayName: "Сбербанк привилегированные",
};
server.use(
@@ -145,9 +152,9 @@ describe("AccountPage states", () => {
: [position],
}),
),
http.get("*/api/v1/stocks/:uid", () => {
http.get(`*/api/v1/${collection}/:uid`, () => {
requests();
return HttpResponse.json({ instrumentUid: uid, slug: "SBERP" });
return HttpResponse.json({ instrumentUid: uid, slug });
}),
);
configureTestAuth();
@@ -158,9 +165,12 @@ describe("AccountPage states", () => {
});
expect(links).toHaveLength(2);
for (const link of links)
expect(link).toHaveAttribute("href", "/stocks/SBERP");
expect(link).toHaveAttribute(
"href",
`/${collection}/${type === "STOCK" ? slug : uid}`,
);
});
expect(requests).toHaveBeenCalledOnce();
expect(requests).toHaveBeenCalledTimes(type === "STOCK" ? 1 : 0);
},
);
+19 -13
View File
@@ -25,7 +25,7 @@ type AccountPagePosition = Portfolio["positions"][number] & {
returnText: string | null;
returnTone?: AccountPageReturnTone;
logoSrc: string | null;
stockTo?: string;
instrumentTo?: string;
};
type AccountPagePositionGroup = {
@@ -87,13 +87,14 @@ function useAccountPageViewModel(scopeId?: string): AccountPageViewModel {
const { accountId: routeAccountId } = useParams();
const accountId = scopeId ?? routeAccountId;
const portfolio = usePortfolio(accountId);
const stockDetails = useStocks(
const stocks = useStocks(
portfolio.data?.positions.flatMap((position) =>
position.type === "STOCK" && position.instrumentUid
? [position.instrumentUid]
: [],
) ?? [],
);
const instrumentDetails = stocks;
const operations = useBrokerageOperations(accountId, {
limit: 5,
});
@@ -136,7 +137,7 @@ function useAccountPageViewModel(scopeId?: string): AccountPageViewModel {
portfolio: portfolio.data,
positionSections: toAccountPagePositionSections(
portfolio.data,
stockDetails,
instrumentDetails,
),
recentOperations,
recentEvents,
@@ -182,7 +183,7 @@ function toRecentEvents(
function toAccountPagePositionSections(
portfolio: Portfolio,
stockDetails: ReturnType<typeof useStocks> = new Map(),
instrumentDetails: Map<string, { slug: string } | undefined> = new Map(),
): AccountPagePositionSection[] {
const denominators = new Map<string, number>();
for (const position of portfolio.positions) {
@@ -193,9 +194,14 @@ function toAccountPagePositionSections(
(denominators.get(currency) ?? 0) + Number(position.currentValue.value),
);
}
const stocks = toSection("stocks", portfolio, denominators, stockDetails);
const bonds = toSection("bonds", portfolio, denominators, stockDetails);
const other = toSection("other", portfolio, denominators, stockDetails);
const stocks = toSection(
"stocks",
portfolio,
denominators,
instrumentDetails,
);
const bonds = toSection("bonds", portfolio, denominators, instrumentDetails);
const other = toSection("other", portfolio, denominators, instrumentDetails);
return [stocks, bonds, other].filter((section) => section.groups.length > 0);
}
@@ -203,7 +209,7 @@ function toSection(
kind: PositionSectionKind,
portfolio: Portfolio,
denominators: Map<string, number>,
stockDetails: ReturnType<typeof useStocks>,
instrumentDetails: Map<string, { slug: string } | undefined>,
): AccountPagePositionSection {
return {
kind,
@@ -223,7 +229,7 @@ function toSection(
position,
denominators,
kind,
stockDetails.get(position.instrumentUid ?? "")?.slug,
instrumentDetails.get(position.instrumentUid ?? "")?.slug,
),
),
),
@@ -265,7 +271,7 @@ function toPosition(
position: Portfolio["positions"][number],
denominators: Map<string, number>,
kind: PositionSectionKind,
stockSlug: string | undefined,
instrumentSlug: string | undefined,
): AccountPagePosition {
const expanded = kind !== "other";
const returnValue = position.absoluteReturn
@@ -279,9 +285,9 @@ function toPosition(
: "negative";
return {
...position,
stockTo:
kind === "stocks" && position.instrumentUid
? `/stocks/${encodeURIComponent(stockSlug ?? position.instrumentUid)}`
instrumentTo:
(kind === "stocks" || kind === "bonds") && position.instrumentUid
? `/${kind}/${encodeURIComponent(instrumentSlug ?? position.instrumentUid)}`
: undefined,
shareText: position.currentValue?.currency
? formatShare(position.currentValue, {
@@ -30,7 +30,7 @@ const PositionCard: FC<PositionCardProps> = (props) => {
value: position.returnText,
})
: null;
const stockTo = position.stockTo;
const instrumentTo = position.instrumentTo;
const identifier =
kind === "stocks" ? (position.ticker ?? position.isin) : position.isin;
const identifierText = [
@@ -69,11 +69,11 @@ const PositionCard: FC<PositionCardProps> = (props) => {
key={position.logoSrc ?? "no-logo"}
src={position.logoSrc}
label={position.displayName}
to={stockTo}
to={instrumentTo}
/>
<Stack gap={2} miw={0} className={classes.expandedIdentity}>
{stockTo ? (
<Anchor component={Link} to={stockTo} c="inherit" fw={600}>
{instrumentTo ? (
<Anchor component={Link} to={instrumentTo} c="inherit" fw={600}>
{position.displayName}
</Anchor>
) : (
@@ -19,7 +19,7 @@ const PositionGroupCard: FC<PositionGroupCardProps> = (props) => {
const { group, kind } = props;
const { t } = useTranslation();
const identity = group.positions[0];
const stockTo = identity.stockTo;
const instrumentTo = identity.instrumentTo;
return (
<Card className={classes.tile} withBorder>
<Group wrap="nowrap" gap="sm">
@@ -28,12 +28,12 @@ const PositionGroupCard: FC<PositionGroupCardProps> = (props) => {
key={identity.logoSrc ?? "no-logo"}
src={identity.logoSrc}
label={identity.displayName}
to={stockTo}
to={instrumentTo}
/>
) : null}
<Stack gap={2} miw={0}>
{stockTo ? (
<Anchor component={Link} to={stockTo} c="inherit" fw={600}>
{instrumentTo ? (
<Anchor component={Link} to={instrumentTo} c="inherit" fw={600}>
{identity.displayName}
</Anchor>
) : (