fix: separate portfolio cash presentation
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import { type QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { type QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import {
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
within,
|
||||||
|
} from "@testing-library/react";
|
||||||
import { HttpResponse, http } from "msw";
|
import { HttpResponse, http } from "msw";
|
||||||
import { MemoryRouter, Route, Routes } from "react-router";
|
import { MemoryRouter, Route, Routes } from "react-router";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
@@ -145,11 +151,39 @@ describe("AccountPage states", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("shows only supplied totals despite cash and positions with unknown value", () => {
|
it("shows supplied cash separately without adding currency instruments or recalculating totals", () => {
|
||||||
renderPage(<AccountPageLoaded {...loadedProps()} />);
|
renderPage(
|
||||||
|
<AccountPageLoaded
|
||||||
|
{...loadedProps({
|
||||||
|
...portfolio,
|
||||||
|
positions: [
|
||||||
|
...portfolio.positions,
|
||||||
|
{
|
||||||
|
...portfolio.positions[0],
|
||||||
|
figi: "FIGI-CURRENCY",
|
||||||
|
displayName: "Торгуемая валюта",
|
||||||
|
type: "CURRENCY",
|
||||||
|
currentValue: { currency: "RUB", value: "1000" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cashBalances: [
|
||||||
|
{ currency: "RUB", value: "234.500000000000000001" },
|
||||||
|
{ currency: "USD", value: "-0.5" },
|
||||||
|
{ currency: "XYZ", value: "3.00" },
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const balances = within(
|
||||||
|
screen.getByRole("region", { name: "Денежные остатки" }),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
balances.getAllByRole("definition").map((node) => node.textContent),
|
||||||
|
).toEqual(["234,500000000000000001 ₽", "-0,5 $", "3,00 XYZ"]);
|
||||||
expect(
|
expect(
|
||||||
screen.getAllByRole("definition").map((node) => node.textContent),
|
screen.getAllByRole("definition").map((node) => node.textContent),
|
||||||
).toEqual(["1 234,5 ₽"]);
|
).toEqual(["1 234,5 ₽", "234,500000000000000001 ₽", "-0,5 $", "3,00 XYZ"]);
|
||||||
|
expect(screen.queryByText("Торгуемая валюта")).not.toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText(/полная оценка|общая стоимость/i),
|
screen.queryByText(/полная оценка|общая стоимость/i),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
@@ -169,11 +203,58 @@ describe("AccountPage states", () => {
|
|||||||
);
|
);
|
||||||
renderAccountRoute(createTestQueryClient());
|
renderAccountRoute(createTestQueryClient());
|
||||||
await screen.findByRole("heading", { name: "Портфель", level: 1 });
|
await screen.findByRole("heading", { name: "Портфель", level: 1 });
|
||||||
expect(screen.queryByRole("definition")).not.toBeInTheDocument();
|
expect(screen.getByRole("definition")).toHaveTextContent("0,5 $");
|
||||||
expect(screen.queryByText("Портфель пока пуст")).not.toBeInTheDocument();
|
expect(screen.queryByText("Портфель пока пуст")).not.toBeInTheDocument();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ totalsByCurrency: [] },
|
||||||
|
{ totalsByCurrency: [{ currency: "RUB", value: "100" }] },
|
||||||
|
])(
|
||||||
|
"hides the empty other section without emptying a currency-only portfolio (totals: $totalsByCurrency)",
|
||||||
|
async ({ totalsByCurrency }) => {
|
||||||
|
const snapshot: Portfolio = {
|
||||||
|
...portfolio,
|
||||||
|
totalsByCurrency,
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
...portfolio.positions[0],
|
||||||
|
displayName: "Торгуемая валюта",
|
||||||
|
type: "CURRENCY",
|
||||||
|
currentValue: { currency: "RUB", value: "100" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cashBalances: [],
|
||||||
|
};
|
||||||
|
server.use(
|
||||||
|
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||||
|
HttpResponse.json(snapshot),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const queryClient = createTestQueryClient();
|
||||||
|
renderAccountRoute(queryClient);
|
||||||
|
|
||||||
|
await screen.findByRole("heading", { name: "Портфель", level: 1 });
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: "Другие позиции" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Торгуемая валюта")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Портфель пока пуст")).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(
|
||||||
|
screen.getByRole("region", { name: "Денежные остатки" }),
|
||||||
|
).queryByRole("definition"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryAllByRole("definition").map((node) => node.textContent),
|
||||||
|
).toEqual(totalsByCurrency.length ? ["100 ₽"] : []);
|
||||||
|
expect(
|
||||||
|
queryClient.getQueryData(portfolioQueryOptions("account-1").queryKey),
|
||||||
|
).toEqual(snapshot);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("renders loading and empty states", () => {
|
it("renders loading and empty states", () => {
|
||||||
renderPage(<AccountPageLoading />);
|
renderPage(<AccountPageLoading />);
|
||||||
expect(screen.getByRole("status")).toBeInTheDocument();
|
expect(screen.getByRole("status")).toBeInTheDocument();
|
||||||
@@ -613,13 +694,17 @@ describe("AccountPage states", () => {
|
|||||||
),
|
),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText("Вторая")).toBeInTheDocument();
|
expect(screen.getByText("Вторая")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("definition")).toHaveTextContent("1 234,5 ₽");
|
expect(
|
||||||
|
screen.getAllByRole("definition").map((node) => node.textContent),
|
||||||
|
).toEqual(["1 234,5 ₽", "0,5 $"]);
|
||||||
expect(screen.getByText(/Данные получены/).textContent).toBe(
|
expect(screen.getByText(/Данные получены/).textContent).toBe(
|
||||||
snapshotText,
|
snapshotText,
|
||||||
);
|
);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Повторить" }));
|
fireEvent.click(screen.getByRole("button", { name: "Повторить" }));
|
||||||
await waitFor(() => expect(requestCount).toBe(3));
|
await waitFor(() => expect(requestCount).toBe(3));
|
||||||
expect(screen.getByRole("definition")).toHaveTextContent("1 234,5 ₽");
|
expect(
|
||||||
|
screen.getAllByRole("definition").map((node) => node.textContent),
|
||||||
|
).toEqual(["1 234,5 ₽", "0,5 $"]);
|
||||||
expect(screen.getByText(/Данные получены/).textContent).toBe(
|
expect(screen.getByText(/Данные получены/).textContent).toBe(
|
||||||
snapshotText,
|
snapshotText,
|
||||||
);
|
);
|
||||||
@@ -629,10 +714,13 @@ describe("AccountPage states", () => {
|
|||||||
isStale: false,
|
isStale: false,
|
||||||
lastSuccessfulAt: "2026-08-14T11:30:00Z",
|
lastSuccessfulAt: "2026-08-14T11:30:00Z",
|
||||||
totalsByCurrency: [{ currency: "USD", value: "-0.5" }],
|
totalsByCurrency: [{ currency: "USD", value: "-0.5" }],
|
||||||
|
cashBalances: [{ currency: "EUR", value: "2.50" }],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByRole("definition")).toHaveTextContent("-0,5 $"),
|
expect(
|
||||||
|
screen.getAllByRole("definition").map((node) => node.textContent),
|
||||||
|
).toEqual(["-0,5 $", "2,50 €"]),
|
||||||
);
|
);
|
||||||
expect(screen.getByText(/Данные получены/)).toHaveTextContent("11:30");
|
expect(screen.getByText(/Данные получены/)).toHaveTextContent("11:30");
|
||||||
expect(
|
expect(
|
||||||
@@ -666,7 +754,9 @@ describe("AccountPage states", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(requestCount).toBe(2));
|
await waitFor(() => expect(requestCount).toBe(2));
|
||||||
expect(screen.getByText("Вторая")).toBeInTheDocument();
|
expect(screen.getByText("Вторая")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("definition")).toHaveTextContent("1 234,5 ₽");
|
expect(
|
||||||
|
screen.getAllByRole("definition").map((node) => node.textContent),
|
||||||
|
).toEqual(["1 234,5 ₽", "0,5 $"]);
|
||||||
expect(screen.getByText(/Данные получены/)).toHaveTextContent("10:00");
|
expect(screen.getByText(/Данные получены/)).toHaveTextContent("10:00");
|
||||||
expect(
|
expect(
|
||||||
screen.queryByRole("status", { name: "Загрузка" }),
|
screen.queryByRole("status", { name: "Загрузка" }),
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import type { Portfolio } from "@/entities/portfolio";
|
|||||||
import { configureAuthClient } from "@/shared/api";
|
import { configureAuthClient } from "@/shared/api";
|
||||||
import { createQueryWrapper } from "@/test/query";
|
import { createQueryWrapper } from "@/test/query";
|
||||||
import { server } from "@/test/server";
|
import { server } from "@/test/server";
|
||||||
import { useAccountPageViewModel } from "./AccountPage.vm";
|
import {
|
||||||
|
toAccountPagePositionSections,
|
||||||
|
useAccountPageViewModel,
|
||||||
|
} from "./AccountPage.vm";
|
||||||
|
|
||||||
const portfolio: Portfolio = {
|
const portfolio: Portfolio = {
|
||||||
lastSuccessfulAt: "2026-08-13T10:00:00Z",
|
lastSuccessfulAt: "2026-08-13T10:00:00Z",
|
||||||
@@ -130,6 +133,55 @@ function mockLoadedAccountPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("useAccountPageViewModel", () => {
|
describe("useAccountPageViewModel", () => {
|
||||||
|
it("excludes currency instruments only from display, preserving order and share denominators", () => {
|
||||||
|
const otherTypes = [
|
||||||
|
"OPTION",
|
||||||
|
"OTHER",
|
||||||
|
"FUTURE",
|
||||||
|
"STRUCTURED_NOTE",
|
||||||
|
"DIGITAL_FINANCIAL_ASSET",
|
||||||
|
"FUND",
|
||||||
|
] as const;
|
||||||
|
const snapshot: Portfolio = {
|
||||||
|
...portfolio,
|
||||||
|
totalsByCurrency: [{ currency: "RUB", value: "1250" }],
|
||||||
|
cashBalances: [{ currency: "RUB", value: "1000" }],
|
||||||
|
positions: [
|
||||||
|
...portfolio.positions,
|
||||||
|
{
|
||||||
|
...portfolio.positions[0],
|
||||||
|
figi: "FIGI-CURRENCY",
|
||||||
|
type: "CURRENCY",
|
||||||
|
currentValue: { currency: "RUB", value: "50" },
|
||||||
|
},
|
||||||
|
...otherTypes.map((type) => ({
|
||||||
|
...portfolio.positions[2],
|
||||||
|
figi: `FIGI-${type}-NEXT`,
|
||||||
|
type,
|
||||||
|
currentValue:
|
||||||
|
type === "OPTION" ? { currency: "RUB", value: "50" } : null,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const original = structuredClone(snapshot);
|
||||||
|
|
||||||
|
const sections = toAccountPagePositionSections(snapshot);
|
||||||
|
|
||||||
|
expect(sections.map((section) => section.kind)).toEqual([
|
||||||
|
"stocks",
|
||||||
|
"bonds",
|
||||||
|
"other",
|
||||||
|
]);
|
||||||
|
expect(sections[2].positions.map((position) => position.figi)).toEqual([
|
||||||
|
"FIGI-FUND",
|
||||||
|
...otherTypes.map((type) => `FIGI-${type}-NEXT`),
|
||||||
|
]);
|
||||||
|
expect(sections[0].positions[0].shareText).toBe("40 %");
|
||||||
|
expect(sections[1].positions[0].shareText).toBe("20 %");
|
||||||
|
expect(sections[2].positions[1].shareText).toBe("20 %");
|
||||||
|
expect(snapshot).toEqual(original);
|
||||||
|
});
|
||||||
|
|
||||||
it("prepares grouped position sections with derived display data", async () => {
|
it("prepares grouped position sections with derived display data", async () => {
|
||||||
configureTestAuth();
|
configureTestAuth();
|
||||||
mockLoadedAccountPage();
|
mockLoadedAccountPage();
|
||||||
|
|||||||
@@ -202,7 +202,11 @@ function toSection(
|
|||||||
.filter((position) => {
|
.filter((position) => {
|
||||||
if (kind === "stocks") return position.type === "STOCK";
|
if (kind === "stocks") return position.type === "STOCK";
|
||||||
if (kind === "bonds") return position.type === "BOND";
|
if (kind === "bonds") return position.type === "BOND";
|
||||||
return position.type !== "STOCK" && position.type !== "BOND";
|
return (
|
||||||
|
position.type !== "STOCK" &&
|
||||||
|
position.type !== "BOND" &&
|
||||||
|
position.type !== "CURRENCY"
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.map((position) => toPosition(position, denominators, kind)),
|
.map((position) => toPosition(position, denominators, kind)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import type {
|
|||||||
AccountPageRecentOperations,
|
AccountPageRecentOperations,
|
||||||
} from "../AccountPage.vm";
|
} from "../AccountPage.vm";
|
||||||
import { AccountHeading } from "./AccountHeading.view";
|
import { AccountHeading } from "./AccountHeading.view";
|
||||||
import { CashBalances } from "./CashBalances.view";
|
import { CurrencyCards } from "./CurrencyCards.view";
|
||||||
import { IncomeSummary } from "./IncomeSummary.view";
|
import { IncomeSummary } from "./IncomeSummary.view";
|
||||||
import { PortfolioRefreshState } from "./PortfolioRefreshState.view";
|
import { PortfolioRefreshState } from "./PortfolioRefreshState.view";
|
||||||
import { PortfolioTotals } from "./PortfolioTotals.view";
|
|
||||||
import { PositionSection } from "./PositionSection.view";
|
import { PositionSection } from "./PositionSection.view";
|
||||||
import { RecentEvents } from "./RecentEvents.view";
|
import { RecentEvents } from "./RecentEvents.view";
|
||||||
import { RecentOperations } from "./RecentOperations.view";
|
import { RecentOperations } from "./RecentOperations.view";
|
||||||
@@ -50,7 +49,7 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
|||||||
date: formatSnapshotTime(props.portfolio.lastSuccessfulAt),
|
date: formatSnapshotTime(props.portfolio.lastSuccessfulAt),
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<PortfolioTotals totals={props.portfolio.totalsByCurrency} />
|
<CurrencyCards amounts={props.portfolio.totalsByCurrency} />
|
||||||
<Accordion
|
<Accordion
|
||||||
multiple
|
multiple
|
||||||
defaultValue={[
|
defaultValue={[
|
||||||
@@ -80,7 +79,7 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
|||||||
<Accordion.Item value="cash">
|
<Accordion.Item value="cash">
|
||||||
<Accordion.Control>{t("portfolio.cashBalances")}</Accordion.Control>
|
<Accordion.Control>{t("portfolio.cashBalances")}</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<CashBalances balances={props.portfolio.cashBalances} />
|
<CurrencyCards amounts={props.portfolio.cashBalances} />
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
<Accordion.Item value="recentOperations">
|
<Accordion.Item value="recentOperations">
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Paper, Text } from "@mantine/core";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import type { Portfolio } from "@/entities/portfolio";
|
|
||||||
import { formatMoney } from "@/shared/lib";
|
|
||||||
import classes from "../AccountPage.module.css";
|
|
||||||
|
|
||||||
type CashBalancesProps = {
|
|
||||||
balances: Portfolio["cashBalances"];
|
|
||||||
};
|
|
||||||
|
|
||||||
const CashBalances: FC<CashBalancesProps> = (props) => {
|
|
||||||
const { balances } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{balances.map((balance) => (
|
|
||||||
<Paper className={classes.cash} key={balance.currency} withBorder>
|
|
||||||
<Text>{balance.currency}</Text>
|
|
||||||
<Text fw={700}>{formatMoney(balance)}</Text>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { CashBalances };
|
|
||||||
+6
-6
@@ -1,9 +1,9 @@
|
|||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { PortfolioTotals } from "./PortfolioTotals.view";
|
import { CurrencyCards } from "./CurrencyCards.view";
|
||||||
|
|
||||||
describe("PortfolioTotals currency presentation", () => {
|
describe("CurrencyCards currency presentation", () => {
|
||||||
it.each([0, 1, 3, 5])(
|
it.each([0, 1, 3, 5])(
|
||||||
"renders only the %i supplied totals in upstream order",
|
"renders only the %i supplied totals in upstream order",
|
||||||
(count) => {
|
(count) => {
|
||||||
@@ -16,7 +16,7 @@ describe("PortfolioTotals currency presentation", () => {
|
|||||||
].slice(0, count);
|
].slice(0, count);
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
<PortfolioTotals totals={totals} />
|
<CurrencyCards amounts={totals} />
|
||||||
</MantineProvider>,
|
</MantineProvider>,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
@@ -52,7 +52,7 @@ describe("PortfolioTotals currency presentation", () => {
|
|||||||
(currency, value, expected, name, symbol) => {
|
(currency, value, expected, name, symbol) => {
|
||||||
render(
|
render(
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
<PortfolioTotals totals={[{ currency, value }]} />
|
<CurrencyCards amounts={[{ currency, value }]} />
|
||||||
</MantineProvider>,
|
</MantineProvider>,
|
||||||
);
|
);
|
||||||
const term = screen.getByRole("term");
|
const term = screen.getByRole("term");
|
||||||
@@ -70,8 +70,8 @@ describe("PortfolioTotals currency presentation", () => {
|
|||||||
it("keeps USD and CAD as distinct supplied values", () => {
|
it("keeps USD and CAD as distinct supplied values", () => {
|
||||||
render(
|
render(
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
<PortfolioTotals
|
<CurrencyCards
|
||||||
totals={[
|
amounts={[
|
||||||
{ currency: "USD", value: "1.00" },
|
{ currency: "USD", value: "1.00" },
|
||||||
{ currency: "CAD", value: "2.00" },
|
{ currency: "CAD", value: "2.00" },
|
||||||
]}
|
]}
|
||||||
+11
-11
@@ -5,12 +5,12 @@ import type { Portfolio } from "@/entities/portfolio";
|
|||||||
import { formatDecimal } from "@/shared/lib";
|
import { formatDecimal } from "@/shared/lib";
|
||||||
import classes from "../AccountPage.module.css";
|
import classes from "../AccountPage.module.css";
|
||||||
|
|
||||||
type PortfolioTotalsProps = {
|
type CurrencyCardsProps = {
|
||||||
totals: Portfolio["totalsByCurrency"];
|
amounts: Portfolio["totalsByCurrency"];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PortfolioTotals: FC<PortfolioTotalsProps> = (props) => {
|
const CurrencyCards: FC<CurrencyCardsProps> = (props) => {
|
||||||
const { totals } = props;
|
const { amounts } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currencies: Record<string, { symbol: string; name: string }> = {
|
const currencies: Record<string, { symbol: string; name: string }> = {
|
||||||
RUB: { symbol: "₽", name: t("portfolio.currencies.RUB") },
|
RUB: { symbol: "₽", name: t("portfolio.currencies.RUB") },
|
||||||
@@ -20,22 +20,22 @@ const PortfolioTotals: FC<PortfolioTotalsProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing={10}>
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing={10}>
|
||||||
{totals.map((total) => {
|
{amounts.map((money) => {
|
||||||
const amount = formatDecimal(total.value);
|
const amount = formatDecimal(money.value);
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
component="dl"
|
component="dl"
|
||||||
m={0}
|
m={0}
|
||||||
className={classes.total}
|
className={classes.total}
|
||||||
key={total.currency}
|
key={money.currency}
|
||||||
withBorder
|
withBorder
|
||||||
>
|
>
|
||||||
<Text component="dt" className={classes.totalBadge}>
|
<Text component="dt" className={classes.totalBadge}>
|
||||||
<span aria-hidden="true">
|
<span aria-hidden="true">
|
||||||
{currencies[total.currency]?.symbol ?? total.currency}
|
{currencies[money.currency]?.symbol ?? money.currency}
|
||||||
</span>
|
</span>
|
||||||
<VisuallyHidden>
|
<VisuallyHidden>
|
||||||
{currencies[total.currency]?.name ?? total.currency}
|
{currencies[money.currency]?.name ?? money.currency}
|
||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
</Text>
|
</Text>
|
||||||
<Text component="dd" m={0} className={classes.totalAmount}>
|
<Text component="dd" m={0} className={classes.totalAmount}>
|
||||||
@@ -49,7 +49,7 @@ const PortfolioTotals: FC<PortfolioTotalsProps> = (props) => {
|
|||||||
) : (
|
) : (
|
||||||
amount
|
amount
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{currencies[total.currency]?.symbol ?? total.currency}
|
{currencies[money.currency]?.symbol ?? money.currency}
|
||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
@@ -58,4 +58,4 @@ const PortfolioTotals: FC<PortfolioTotalsProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { PortfolioTotals };
|
export { CurrencyCards };
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Stack } from "@mantine/core";
|
import { Stack } from "@mantine/core";
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
|
import { CurrencyCards } from "./CurrencyCards.view";
|
||||||
import { PortfolioRefreshState } from "./PortfolioRefreshState.view";
|
import { PortfolioRefreshState } from "./PortfolioRefreshState.view";
|
||||||
import { PortfolioTotals } from "./PortfolioTotals.view";
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "Product/PortfolioRefreshState",
|
title: "Product/PortfolioRefreshState",
|
||||||
@@ -18,14 +18,14 @@ const meta = {
|
|||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
component:
|
component:
|
||||||
"Обновление уже показанных данных портфеля. Реальные PortfolioTotals остаются видимыми вместе с PortfolioRefreshState при refetch и его ошибке. Суммы синтетические; это presentation Client Cached Snapshot, без QueryClient и запроса API. Retry регистрирует локальное действие в Actions и не доказывает восстановление API.",
|
"Обновление уже показанных данных портфеля. Реальные CurrencyCards остаются видимыми вместе с PortfolioRefreshState при refetch и его ошибке. Суммы синтетические; это presentation Client Cached Snapshot, без QueryClient и запроса API. Retry регистрирует локальное действие в Actions и не доказывает восстановление API.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<Stack>
|
<Stack>
|
||||||
<PortfolioTotals
|
<CurrencyCards
|
||||||
totals={[
|
amounts={[
|
||||||
{ currency: "RUB", value: "125000.50" },
|
{ currency: "RUB", value: "125000.50" },
|
||||||
{ currency: "USD", value: "320" },
|
{ currency: "USD", value: "320" },
|
||||||
]}
|
]}
|
||||||
|
|||||||
Reference in New Issue
Block a user