feat: group all-account rows by instrument and day (WealthLedger/main#47) (#126)
This commit was merged in pull request #126.
This commit is contained in:
@@ -100,7 +100,10 @@ function money(currency: string, value: string) {
|
||||
return { currency, value };
|
||||
}
|
||||
|
||||
function renderPage(search = "from=2026-08-01&to=2026-08-27") {
|
||||
function renderPage(
|
||||
search = "from=2026-08-01&to=2026-08-27",
|
||||
accountId = "account-1",
|
||||
) {
|
||||
configureAuthClient({
|
||||
accessToken: () => "token",
|
||||
clear: vi.fn(),
|
||||
@@ -113,7 +116,7 @@ function renderPage(search = "from=2026-08-01&to=2026-08-27") {
|
||||
<DatesProvider settings={{ locale: "ru" }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter
|
||||
initialEntries={[`/accounts/account-1/income?${search}`]}
|
||||
initialEntries={[`/accounts/${accountId}/income?${search}`]}
|
||||
>
|
||||
<LocationProbe />
|
||||
|
||||
@@ -680,4 +683,110 @@ describe("income period and snapshot boundary", () => {
|
||||
expect(screen.queryByText(/Итого:|Выплат:/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("groups all-account payouts by instrument with an exact total", async () => {
|
||||
server.use(
|
||||
http.get(
|
||||
"*/api/v1/brokerage-accounts/:accountId/income-analytics",
|
||||
() => HttpResponse.json(resource({
|
||||
...analytics,
|
||||
accountId: "all",
|
||||
entries: [
|
||||
{
|
||||
...entry(),
|
||||
account: { id: "broker", name: "Брокерский" },
|
||||
payment: money("RUB", "10"),
|
||||
},
|
||||
{
|
||||
...entry(),
|
||||
account: { id: "iis", name: "ИИС" },
|
||||
id: "coupon-2",
|
||||
payment: money("RUB", "20"),
|
||||
},
|
||||
],
|
||||
})),
|
||||
),
|
||||
);
|
||||
renderPage(undefined, "all");
|
||||
|
||||
expect(await screen.findByText("Купон · BOND")).toBeInTheDocument();
|
||||
expect(screen.getByText("+30 ₽")).toBeInTheDocument();
|
||||
|
||||
const accounts = within(screen.getByRole("list", {
|
||||
name: /Купон · BOND, Исполнено/,
|
||||
}));
|
||||
|
||||
expect(accounts.getByText("Брокерский")).toBeInTheDocument();
|
||||
expect(accounts.getByText("ИИС")).toBeInTheDocument();
|
||||
expect(accounts.queryByRole("link")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps repeated payout ids inside one group as separate cards", async () => {
|
||||
server.use(
|
||||
http.get(
|
||||
"*/api/v1/brokerage-accounts/:accountId/income-analytics",
|
||||
() => HttpResponse.json(resource({
|
||||
...analytics,
|
||||
accountId: "all",
|
||||
entries: [
|
||||
{
|
||||
...entry(),
|
||||
account: { id: "broker", name: "Брокерский" },
|
||||
payment: money("RUB", "10"),
|
||||
},
|
||||
{
|
||||
...entry(),
|
||||
account: { id: "iis", name: "ИИС" },
|
||||
payment: money("RUB", "20"),
|
||||
},
|
||||
],
|
||||
})),
|
||||
),
|
||||
);
|
||||
renderPage(undefined, "all");
|
||||
|
||||
expect(await screen.findByText("+30 ₽")).toBeInTheDocument();
|
||||
|
||||
const accounts = within(screen.getByRole("list", {
|
||||
name: /Купон · BOND/,
|
||||
}));
|
||||
|
||||
expect(accounts.getAllByRole("listitem")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("shows the group total after completeness changes to complete", async () => {
|
||||
let completeness: "complete" | "incomplete" = "incomplete";
|
||||
|
||||
const entries = [
|
||||
{
|
||||
...entry(),
|
||||
account: { id: "broker", name: "Брокерский" },
|
||||
payment: money("RUB", "10"),
|
||||
},
|
||||
{
|
||||
...entry(),
|
||||
account: { id: "iis", name: "ИИС" },
|
||||
id: "coupon-2",
|
||||
payment: money("RUB", "20"),
|
||||
},
|
||||
];
|
||||
|
||||
server.use(
|
||||
http.get(
|
||||
"*/api/v1/brokerage-accounts/:accountId/income-analytics",
|
||||
() => HttpResponse.json(
|
||||
resource({ ...analytics, accountId: "all", entries }, { completeness }),
|
||||
),
|
||||
),
|
||||
);
|
||||
const view = renderPage(undefined, "all");
|
||||
|
||||
await screen.findByText("Купон · BOND");
|
||||
expect(screen.queryByText("+30 ₽")).not.toBeInTheDocument();
|
||||
view.unmount();
|
||||
|
||||
completeness = "complete";
|
||||
renderPage(undefined, "all");
|
||||
expect(await screen.findByText("+30 ₽")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ type AccountIncomePageViewModel
|
||||
onSubmit: (values: IncomeForm) => void;
|
||||
partial?: boolean;
|
||||
state: "loaded";
|
||||
totalVisible: boolean;
|
||||
}
|
||||
| {
|
||||
period: IncomePeriod;
|
||||
@@ -116,6 +117,7 @@ const useAccountIncomePageViewModel = (): AccountIncomePageViewModel => {
|
||||
onSubmit,
|
||||
partial: analytics.meta?.completeness === "incomplete",
|
||||
state: "loaded",
|
||||
totalVisible: analytics.meta?.completeness === "complete",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+5
-1
@@ -53,7 +53,11 @@ const AccountIncomePageLoaded: FC<LoadedProps> = (props) => {
|
||||
|
||||
<Alert color="blue">{t("income.taxBoundary")}</Alert>
|
||||
|
||||
<IncomePayments entries={display.entries} />
|
||||
<IncomePayments
|
||||
allAccounts={props.analytics.accountId === "all"}
|
||||
entries={display.entries}
|
||||
totalVisible={props.totalVisible}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,13 +3,22 @@ import type { FC } from "react";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { LedgerEntryRow } from "@/entities/broker-operation";
|
||||
import {
|
||||
brokerageOperationGroupKey,
|
||||
LedgerEntryRow,
|
||||
OperationGroup,
|
||||
} from "@/entities/broker-operation";
|
||||
import { groupItems } from "@/shared/lib";
|
||||
|
||||
import type { IncomeDisplayModel } from "../../../model/display";
|
||||
|
||||
import classes from "../AccountIncomePage.module.css";
|
||||
|
||||
type IncomePaymentsProps = { entries: IncomeDisplayModel["entries"] };
|
||||
type IncomePaymentsProps = {
|
||||
allAccounts?: boolean;
|
||||
entries: IncomeDisplayModel["entries"];
|
||||
totalVisible?: boolean;
|
||||
};
|
||||
|
||||
const IncomePayments: FC<IncomePaymentsProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -19,11 +28,24 @@ const IncomePayments: FC<IncomePaymentsProps> = (props) => {
|
||||
<Title order={2}>{t("income.payments")}</Title>
|
||||
|
||||
<Stack className={classes.entries} component="ul" gap="sm">
|
||||
{props.entries.map(({ key, operation }) => (
|
||||
<li key={key}>
|
||||
<LedgerEntryRow operation={operation} wrap />
|
||||
</li>
|
||||
))}
|
||||
{groupItems(
|
||||
props.entries,
|
||||
entry => props.allAccounts
|
||||
? brokerageOperationGroupKey(entry.operation)
|
||||
: null,
|
||||
).map(entry => entry.type === "group"
|
||||
? (
|
||||
<OperationGroup
|
||||
entries={entry.items}
|
||||
key={entry.key}
|
||||
totalVisible={props.totalVisible === true}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<li key={entry.item.key}>
|
||||
<LedgerEntryRow operation={entry.item.operation} wrap />
|
||||
</li>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user