feat: add all-accounts portfolio pages

Reuse account sections and group securities with per-account holdings.

Use Mantine Select to preserve filters and reset pagination on scope changes.

Document component reuse guardrails and cover routes, grouping and scope switching.
This commit is contained in:
2026-09-09 20:36:49 +03:00
parent c906dfa2e2
commit daf1dca666
36 changed files with 1962 additions and 117 deletions
+4 -2
View File
@@ -13,8 +13,10 @@ const AccountBreadcrumbs: FC<AccountBreadcrumbsProps> = (props) => {
const { data: accounts } = useBrokerageAccounts();
const { t } = useTranslation();
const accountName =
accounts?.find((account) => account.id === accountId)?.name ??
t("breadcrumbs.accountFallback");
accountId === "all"
? t("accounts.all")
: (accounts?.find((account) => account.id === accountId)?.name ??
t("breadcrumbs.accountFallback"));
const accountPath = `/accounts/${accountId}`;
const items = [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
+45
View File
@@ -0,0 +1,45 @@
import { Select } from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router";
import { useBrokerageAccounts } from "@/entities/brokerage-account";
type AccountScopeSwitchProps = { accountId: string; section?: string };
const AccountScopeSwitch: FC<AccountScopeSwitchProps> = (props) => {
const { accountId, section } = props;
const { data: accounts } = useBrokerageAccounts();
const { t } = useTranslation();
const navigate = useNavigate();
const { search } = useLocation();
const data = [
{ value: "all", label: t("accounts.all") },
...(accounts ?? []).map((account) => ({
value: account.id,
label: account.name,
})),
];
if (!data.some((item) => item.value === accountId))
data.push({ value: accountId, label: t("breadcrumbs.accountFallback") });
return (
<Select
label={t("accounts.scope")}
value={accountId}
data={data}
allowDeselect={false}
maw={360}
w="100%"
onChange={(value) => {
if (!value || value === accountId) return;
const filters = new URLSearchParams(search);
filters.delete("cursor");
navigate({
pathname: `/accounts/${encodeURIComponent(value)}${section ? `/${section}` : ""}`,
search: filters.toString(),
});
}}
/>
);
};
export { AccountScopeSwitch };
+4 -2
View File
@@ -1,5 +1,5 @@
import { MantineProvider } from "@mantine/core";
import { render, screen } from "@testing-library/react";
import { render, screen, within } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AppBreadcrumbs } from "./AppBreadcrumbs";
@@ -77,7 +77,9 @@ describe("AppBreadcrumbs", () => {
it("uses the account fallback without exposing the identifier", () => {
renderBreadcrumbs("/accounts/account-1");
expect(screen.getByText("Счет")).toHaveAttribute("aria-current", "page");
expect(
within(screen.getByRole("navigation")).getByText("Счет"),
).toHaveAttribute("aria-current", "page");
expect(screen.queryByText("account-1")).not.toBeInTheDocument();
});
+38 -13
View File
@@ -1,7 +1,9 @@
import { Stack } from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { useMatch } from "react-router";
import { AccountBreadcrumbs } from "./AccountBreadcrumbs";
import { AccountScopeSwitch } from "./AccountScopeSwitch";
import { BreadcrumbTrail } from "./BreadcrumbTrail";
const AppBreadcrumbs: FC = () => {
@@ -18,27 +20,50 @@ const AppBreadcrumbs: FC = () => {
if (operations)
return (
<AccountBreadcrumbs
accountId={operations.params.accountId ?? ""}
section="operations"
/>
<Stack gap="sm" mb="lg">
<AccountBreadcrumbs
accountId={operations.params.accountId ?? ""}
section="operations"
/>
<AccountScopeSwitch
accountId={operations.params.accountId ?? ""}
section="operations"
/>
</Stack>
);
if (events)
return (
<AccountBreadcrumbs
accountId={events.params.accountId ?? ""}
section="events"
/>
<Stack gap="sm" mb="lg">
<AccountBreadcrumbs
accountId={events.params.accountId ?? ""}
section="events"
/>
<AccountScopeSwitch
accountId={events.params.accountId ?? ""}
section="events"
/>
</Stack>
);
if (income)
return (
<AccountBreadcrumbs
accountId={income.params.accountId ?? ""}
section="income"
/>
<Stack gap="sm" mb="lg">
<AccountBreadcrumbs
accountId={income.params.accountId ?? ""}
section="income"
/>
<AccountScopeSwitch
accountId={income.params.accountId ?? ""}
section="income"
/>
</Stack>
);
if (account)
return <AccountBreadcrumbs accountId={account.params.accountId ?? ""} />;
return (
<Stack gap="sm" mb="lg">
<AccountBreadcrumbs accountId={account.params.accountId ?? ""} />
<AccountScopeSwitch accountId={account.params.accountId ?? ""} />
</Stack>
);
if (accounts)
return <BreadcrumbTrail items={[{ label: t("breadcrumbs.accounts") }]} />;
if (settings)