feat(cards): refine controls and modal transitions

This commit is contained in:
2026-09-12 22:34:27 +03:00
parent cf21a5bd17
commit aba4cf1ce8
16 changed files with 245 additions and 107 deletions
+35 -3
View File
@@ -42,7 +42,7 @@ function renderCards(fail = false, categoryFail = false) {
);
return render(
<QueryClientProvider client={createTestQueryClient()}>
<MantineProvider>
<MantineProvider env="test">
<MemoryRouter initialEntries={["/cards?month=2026-09"]}>
<CardsPage />
</MemoryRouter>
@@ -138,10 +138,42 @@ describe("CardsPage", () => {
fireEvent.click(screen.getByLabelText("Показать архив"));
await screen.findByText("Запасная");
expect(requests).toContain("?includeArchived=true");
expect(screen.getByLabelText("Месяц")).toHaveTextContent("September 2026");
expect(screen.getByLabelText("Период")).toHaveTextContent(
"01.09.2026 – 30.09.2026",
);
expect(screen.getByRole("link", { name: /Запасная/ })).toHaveAttribute(
"href",
"/cards/demo?month=2026-09",
"/cards/demo?from=2026-09-01&to=2026-09-30",
);
});
it("applies a completed custom period to the summary", async () => {
renderCards();
await screen.findByText("За этот период операций по расходам нет.");
await screen.findByRole("heading", { name: "Добавьте первую карту" });
const requests: string[] = [];
server.use(
http.get("*/api/v1/bank-cards/summary", ({ request }) => {
requests.push(new URL(request.url).search);
return HttpResponse.json({
cardId: null,
expenses: money("0"),
refunds: money("0"),
net: money("0"),
categories: [],
});
}),
);
fireEvent.click(screen.getByLabelText("Период"));
fireEvent.click(
await screen.findByRole("button", { name: "10 September 2026" }),
);
expect(requests).toHaveLength(0);
fireEvent.click(
await screen.findByRole("button", { name: "20 September 2026" }),
);
await waitFor(() =>
expect(requests.at(-1)).toBe("?from=2026-09-10&to=2026-09-20"),
);
});
it("shows retry after failure instead of presenting an empty list", async () => {
+49 -20
View File
@@ -1,4 +1,5 @@
import {
ActionIcon,
Button,
Group,
Paper,
@@ -9,13 +10,13 @@ import {
Text,
Title,
} from "@mantine/core";
import { MonthPickerInput } from "@mantine/dates";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import {
BankCardFace,
BankFeedback,
BankPeriodPicker,
BankSummaryView,
} from "@/entities/bank-card";
import { LoadingSkeleton, StateView } from "@/shared/ui";
@@ -28,11 +29,29 @@ const CardsPageView: FC<CardsPageViewProps> = (props) => {
const { t } = useTranslation();
return (
<Stack gap="lg">
<Group justify="space-between">
<Group gap="sm">
<Title order={1}>{t("bank.title")}</Title>
<Button onClick={() => vm.setAdding(true)}>{t("bank.addCard")}</Button>
<ActionIcon
size="lg"
variant="light"
aria-label={t("bank.addCard")}
title={t("bank.addCard")}
onClick={() => vm.setAdding(true)}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
aria-hidden="true"
>
<path d="M12 5v14M5 12h14" />
</svg>
</ActionIcon>
</Group>
<Text c="dimmed">{t("bank.manualBalance")}</Text>
<Switch
label={t("bank.showArchive")}
checked={vm.archive}
@@ -83,14 +102,32 @@ const CardsPageView: FC<CardsPageViewProps> = (props) => {
{vm.cards.data && vm.cards.isFetching && (
<Text role="status">{t("bank.refreshing")}</Text>
)}
<MonthPickerInput
label={t("bank.month")}
value={`${vm.month}-01`}
onChange={(value) => {
if (value) vm.setMonth(value.slice(0, 7));
}}
popoverProps={{ middlewares: { shift: { crossAxis: true } } }}
/>
<Group align="flex-end" justify="space-between">
<BankPeriodPicker period={vm.period} onChange={vm.setPeriod} />
{Boolean(vm.categories.data?.length || vm.cards.data?.length) && (
<Button
variant="subtle"
color="gray"
onClick={() => vm.setManagingCategories(true)}
leftSection={
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.7"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z" />
</svg>
}
>
{t("bank.categories")}
</Button>
)}
</Group>
<BankSummaryView
all
data={vm.summary.data}
@@ -107,14 +144,6 @@ const CardsPageView: FC<CardsPageViewProps> = (props) => {
retry={() => void vm.categories.refetch()}
/>
)}
{Boolean(vm.categories.data?.length || vm.cards.data?.length) && (
<Button
variant="default"
onClick={() => vm.setManagingCategories(true)}
>
{t("bank.categories")}
</Button>
)}
</Stack>
);
};
+7 -7
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useSearchParams } from "react-router";
import {
bankMonth,
bankPeriod,
useBankCards,
useBankCategories,
useBankSummary,
@@ -9,30 +9,30 @@ import {
function useCardsPageVm() {
const [params, setParams] = useSearchParams();
const month = bankMonth(params.get("month"));
const period = bankPeriod(params);
const [archive, setArchive] = useState(false);
const categories = useBankCategories();
const [managingCategories, setManagingCategories] = useState(false);
const [adding, setAdding] = useState(false);
const cards = useBankCards(archive);
const summary = useBankSummary(month);
const summary = useBankSummary(period);
return {
categories,
managingCategories,
setManagingCategories,
month,
period,
archive,
setArchive,
adding,
setAdding,
cards,
summary,
setMonth: (value: string) => {
if (value) setParams({ month: bankMonth(value) });
setPeriod: (value: [string | null, string | null]) => {
if (value[0] && value[1]) setParams({ from: value[0], to: value[1] });
},
items: cards.data?.map((card) => ({
...card,
to: `/cards/${card.id}?month=${month}`,
to: `/cards/${card.id}?${new URLSearchParams(period)}`,
})),
};
}