feat: group all-account rows by instrument and day (WealthLedger/main#47) (#126)
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 9s
Frontend verify / verify (push) Failing after 6m1s

This commit was merged in pull request #126.
This commit is contained in:
2026-10-30 07:28:19 +03:00
parent 6792f5b70a
commit 71126c91f0
40 changed files with 1564 additions and 102 deletions
@@ -11,6 +11,7 @@ import { http, HttpResponse } from "msw";
import { MemoryRouter, Route, Routes } from "react-router";
import { describe, expect, it, vi } from "vitest";
import type { BrokerageOperation } from "@/entities/broker-operation";
import type { BrokerageEvent } from "@/entities/brokerage-event";
import { type Portfolio, portfolioQueryOptions } from "@/entities/portfolio";
@@ -1084,4 +1085,72 @@ describe("AccountPage states", () => {
expect(screen.queryByRole("region", { name: "Нереализованный результат" }))
.not.toBeInTheDocument();
});
it("groups overview items only after the existing five-item limits", async () => {
const operations = Array.from({ length: 6 }, (_, index) => ({
account: { id: `account-${index}`, name: `Счёт ${index + 1}` },
accruedInterest: null,
category: "income",
commission: null,
description: "Купон",
figi: "FIGI",
id: `operation-${index}`,
instrumentType: "bond",
instrumentUid: "UID",
logoName: null,
occurredAt: "2026-08-14T10:00:00Z",
payment: { currency: "RUB", value: "100" },
price: null,
quantity: null,
ticker: "BOND",
type: "OPERATION_TYPE_COUPON",
})) satisfies BrokerageOperation[];
const events = Array.from({ length: 6 }, (_, index) => ({
account: { id: `account-${index}`, name: `Счёт ${index + 1}` },
date: "2026-08-20",
dateKind: "event",
displayName: "ОФЗ 26238",
figi: "FIGI",
instrumentUid: "UID",
isin: "RU000A0JX0J2",
logoName: null,
projectedAmount: { currency: "RUB", value: "100" },
ticker: "OFZ",
type: "coupon",
})) satisfies BrokerageEvent[];
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json(resource({
accountId: "all",
hasNext: true,
items: operations.slice(0, 5),
nextCursor: "next",
}))),
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
HttpResponse.json(resource({
accountId: "all",
calculatedAt: "2026-08-14T00:00:00Z",
events,
}))),
);
renderAccountRoute(createTestQueryClient(), "all");
await screen.findByRole("heading", { level: 1, name: "Все счета" });
const operationGroup = screen.getByRole("list", {
name: /Купон · BOND, Исполнено/,
});
expect(screen.getByText("+500 ₽")).toBeInTheDocument();
expect(within(operationGroup).getAllByRole("listitem")).toHaveLength(5);
expect(within(operationGroup).queryByRole("link")).not.toBeInTheDocument();
const eventGroup = screen.getByRole("list", {
name: /Купон · ОФЗ 26238, 20\.08\.2026, 500 ₽/,
});
expect(screen.getAllByText("500 ₽").length).toBeGreaterThan(0);
expect(within(eventGroup).getAllByRole("listitem")).toHaveLength(5);
});
});
@@ -46,9 +46,15 @@ const AccountPageEmpty: FC<AccountPageEmptyProps> = (props) => {
structure={{ currencies: [], unvaluedCategories: [] }}
/>
<RecentOperations operations={props.recentOperations} />
<RecentOperations
allAccounts={props.accountId === "all"}
operations={props.recentOperations}
/>
<BrokerageEventsPreview events={props.recentEvents} />
<BrokerageEventsPreview
allAccounts={props.accountId === "all"}
events={props.recentEvents}
/>
{!!props.incomeSummary && <IncomeSummary {...props.incomeSummary} />}
</Stack>
@@ -149,6 +149,7 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
<Tabs.Panel value="operations">
<RecentOperations
allAccounts={props.accountId === "all"}
moreLabel={t("portfolio.more")}
operations={props.recentOperations}
/>
@@ -156,6 +157,7 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
<Tabs.Panel value="events">
<BrokerageEventsPreview
allAccounts={props.accountId === "all"}
events={props.recentEvents}
moreLabel={t("portfolio.more")}
/>
@@ -5,11 +5,17 @@ import { LoadingSkeleton } from "@wealthledger/ui";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { LedgerEntryRow } from "@/entities/broker-operation";
import {
brokerageOperationGroupKey,
LedgerEntryRow,
OperationGroup,
} from "@/entities/broker-operation";
import { groupItems } from "@/shared/lib";
import type { AccountPageRecentOperations } from "../../../model/previews";
type RecentOperationsProps = {
allAccounts?: boolean;
moreLabel?: string;
operations: AccountPageRecentOperations;
};
@@ -32,17 +38,41 @@ const RecentOperations: FC<RecentOperationsProps> = (props) => {
{operations.state === "partial" && <Text role="status">{t("investment.partial")}</Text>}
{(operations.state === "loaded" || operations.state === "partial")
&& operations.items.map(operation => (
<LedgerEntryRow
key={JSON.stringify([
operation.account?.id,
operation.id,
operation.logoName,
])}
operation={operation}
/>
))}
{(operations.state === "loaded" || operations.state === "partial") && (
<Stack
component="ul"
gap="sm"
m={0}
p={0}
style={{ listStyle: "none" }}
>
{groupItems(
operations.items.map(operation => ({
key: JSON.stringify([
operation.account?.id,
operation.id,
operation.logoName,
]),
operation,
})),
entry => props.allAccounts
? brokerageOperationGroupKey(entry.operation)
: null,
).map(entry => entry.type === "group"
? (
<OperationGroup
entries={entry.items}
key={entry.key}
totalVisible={operations.totalVisible === true}
/>
)
: (
<li key={entry.item.key}>
<LedgerEntryRow operation={entry.item.operation} />
</li>
))}
</Stack>
)}
<Button component={Link} to={operations.to} variant="subtle">
{props.moreLabel ?? t("ledger.open")}