feat: add bond pages and portfolio navigation
This commit is contained in:
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user