From 6b310db0b8c37cb4cffe54acf3b7e9799f9f3ea0 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 12 Sep 2026 19:56:12 +0300 Subject: [PATCH] feat: polish bank operation cards and dates --- src/entities/bank-card/index.ts | 2 + .../bank-card/model/form-validation.test.ts | 13 ++ .../bank-card/model/form-validation.ts | 4 + .../bank-card/model/operation-appearance.ts | 118 ++++++++++++++++++ .../bank-card/ui/BankCardForm.test.tsx | 4 +- src/entities/bank-card/ui/BankCardForm.tsx | 3 +- .../bank-card/ui/BankCardForm.view.tsx | 13 +- .../ui/BankOperationIcon.stories.tsx | 68 ++++++++++ .../bank-card/ui/BankOperationIcon.test.tsx | 66 ++++++++++ .../bank-card/ui/BankOperationIcon.tsx | 31 +++++ .../bank-card/ui/BankSummary.view.tsx | 9 +- src/entities/bank-card/ui/CategoryManager.tsx | 5 + src/pages/card/ui/CardPage.test.tsx | 1 + src/pages/card/ui/CardPage.view.tsx | 13 +- src/pages/card/ui/CardPage.vm.ts | 5 +- src/pages/card/ui/DeleteOperation.tsx | 4 +- src/pages/card/ui/OperationForm.test.tsx | 12 +- src/pages/card/ui/OperationForm.tsx | 8 +- src/pages/card/ui/OperationForm.view.tsx | 13 +- src/shared/ui/EntryCard.tsx | 16 ++- 20 files changed, 382 insertions(+), 26 deletions(-) create mode 100644 src/entities/bank-card/model/operation-appearance.ts create mode 100644 src/entities/bank-card/ui/BankOperationIcon.stories.tsx create mode 100644 src/entities/bank-card/ui/BankOperationIcon.test.tsx create mode 100644 src/entities/bank-card/ui/BankOperationIcon.tsx diff --git a/src/entities/bank-card/index.ts b/src/entities/bank-card/index.ts index cfb514d..16cd0b6 100644 --- a/src/entities/bank-card/index.ts +++ b/src/entities/bank-card/index.ts @@ -11,6 +11,7 @@ export { useBankSummary, } from "./api/queries"; export { + bankDateInputValue, bankDateSchema, bankMoneyValue, bankMonth, @@ -20,6 +21,7 @@ export { } from "./model/form-validation"; export { BankCardForm } from "./ui/BankCardForm"; export { BankFeedback } from "./ui/BankFeedback"; +export { BankOperationIcon } from "./ui/BankOperationIcon"; export { BankSummaryView } from "./ui/BankSummary.view"; export { CategoryManager } from "./ui/CategoryManager"; export { useBankFormFocus } from "./ui/useBankFormFocus"; diff --git a/src/entities/bank-card/model/form-validation.test.ts b/src/entities/bank-card/model/form-validation.test.ts index 225e278..aebca68 100644 --- a/src/entities/bank-card/model/form-validation.test.ts +++ b/src/entities/bank-card/model/form-validation.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { bankCardSchema, + bankDateInputValue, bankDateSchema, bankMoneyValue, bankMonth, @@ -54,3 +55,15 @@ describe("bank form input", () => { expect(bankDateSchema.safeParse("2026-02-30").success).toBe(false); }); }); + +it("converts local date input without accepting impossible or unfinished dates", () => { + expect(bankDateInputValue("07.08.2026")).toBe("2026-08-07"); + expect( + bankDateSchema.safeParse(bankDateInputValue("29.02.2024")).success, + ).toBe(true); + for (const value of ["31.02.2026", "07.08.", ""]) { + expect(bankDateSchema.safeParse(bankDateInputValue(value)).success).toBe( + false, + ); + } +}); diff --git a/src/entities/bank-card/model/form-validation.ts b/src/entities/bank-card/model/form-validation.ts index 490866f..dd3d7a2 100644 --- a/src/entities/bank-card/model/form-validation.ts +++ b/src/entities/bank-card/model/form-validation.ts @@ -9,6 +9,9 @@ function bankToday() { day: "2-digit", }).format(new Date()); } +function bankDateInputValue(value: string) { + return value.replace(/^(\d{2})\.(\d{2})\.(\d{4})$/, "$3-$2-$1"); +} function bankMonth(value: string | null) { return value && /^\d{4}-(0[1-9]|1[0-2])$/.test(value) ? value @@ -76,6 +79,7 @@ type BankCardValues = z.infer; export { type BankCardValues, bankCardSchema, + bankDateInputValue, bankDateSchema, bankMoneyValue, bankMonth, diff --git a/src/entities/bank-card/model/operation-appearance.ts b/src/entities/bank-card/model/operation-appearance.ts new file mode 100644 index 0000000..cf5ea23 --- /dev/null +++ b/src/entities/bank-card/model/operation-appearance.ts @@ -0,0 +1,118 @@ +import type { BankOperation } from "../api/queries"; + +type OperationIcon = { path: string; color: string }; + +const categoryIcons: Record = { + алкоголь: { + path: "M7 3h10v5a5 5 0 0 1-10 0V3Z M7 7h10 M12 13v8 M8 21h8", + color: "grape", + }, + вклады: { + path: "M4 8c0-4 16-4 16 0s-16 4-16 0Z M4 8v5c0 4 16 4 16 0V8 M4 13v5c0 4 16 4 16 0v-5", + color: "green", + }, + "инвестиции и накопления": { + path: "M3 3v18h18 M6 15l5-5 4 3 6-8 M16 5h5v5", + color: "green", + }, + образование: { + path: "M2 8l10-5 10 5-10 5L2 8Z M6 10v7c4 3 8 3 12 0v-7 M22 8v9", + color: "blue", + }, + переводы: { path: "M3 7h18l-4-4 M21 17H3l4 4", color: "blue" }, + "погашение кредита": { + path: "M3 5h18v14H3V5Z M3 9h18 M8 14l3 3 5-5", + color: "orange", + }, + подписки: { + path: "M4 10a8 8 0 0 1 13-6l3 3 M20 3v4h-4 M20 14a8 8 0 0 1-13 6l-3-3 M4 21v-4h4", + color: "grape", + }, + путешествия: { + path: "M10 3h4v7l7 5v3l-7-3v5l2 1v1l-4-1-4 1v-1l2-1v-5l-7 3v-3l7-5V3Z", + color: "blue", + }, + "связь и интернет": { + path: "M3 7a14 14 0 0 1 18 0 M6 11a9 9 0 0 1 12 0 M9 15a4 4 0 0 1 6 0 M12 19h.01", + color: "blue", + }, + "снятие наличных": { + path: "M4 10V3h16v7 M7 7h10v14H7V7Z M7 11h10 M10 16h4", + color: "green", + }, + ставки: { + path: "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z M7 7h.01 M17 7h.01 M12 12h.01 M7 17h.01 M17 17h.01", + color: "grape", + }, + страхование: { + path: "M12 2l8 4v6c0 5-8 10-8 10S4 17 4 12V6l8-4Z M8 12l3 3 5-6", + color: "green", + }, + табак: { + path: "M3 14h18v5H3v-5Z M16 14v5 M8 10V8c0-2 3-2 3-4V2 M15 10V8c0-2 3-2 3-4", + color: "orange", + }, + "хостинг и ии-сервисы": { + path: "M3 3h18v7H3V3Z M3 14h18v7H3v-7Z M7 6.5h.01 M7 17.5h.01 M15 6.5h3 M15 17.5h3", + color: "grape", + }, + "цифровые покупки": { + path: "M3 3h18v13H3V3Z M8 21h8 M12 16v5 M12 6v7 M9 10l3 3 3-3", + color: "blue", + }, + продукты: { + path: "M3 9h18l-2 11H5L3 9Z M8 9l4-6 4 6 M9 13v3 M15 13v3", + color: "green", + }, + кафе: { + path: "M4 8h12v7a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8Z M16 9h2a3 3 0 0 1 0 6h-2 M8 3v2 M12 3v2 M3 22h15", + color: "orange", + }, + такси: { + path: "M4 10l2-6h12l2 6 M3 10h18v8H3v-8Z M6 18v3 M18 18v3 M6 14h2 M16 14h2 M10 4V2h4v2", + color: "yellow", + }, + транспорт: { + path: "M6 3h12a2 2 0 0 1 2 2v13H4V5a2 2 0 0 1 2-2Z M4 11h16 M7 15h1 M16 15h1 M7 18v3 M17 18v3 M9 6h6", + color: "blue", + }, + жилье: { path: "M3 11l9-8 9 8 M5 10v11h14V10 M9 21v-8h6v8", color: "orange" }, + здоровье: { path: "M9 3h6v6h6v6h-6v6H9v-6H3V9h6V3Z", color: "red" }, + покупки: { + path: "M5 7h14l2 14H3L5 7Z M8 8V6a4 4 0 0 1 8 0v2", + color: "grape", + }, + развлечения: { + path: "M9 5v14a3 3 0 1 1-3-3h3 M9 5l11-2v14a3 3 0 1 1-3-3h3 M9 9l11-2", + color: "grape", + }, +}; +const otherIcon = { + path: "M4 4h6v6H4V4Z M14 4h6v6h-6V4Z M4 14h6v6H4v-6Z M14 14h6v6h-6v-6Z", + color: "gray", +}; +const operationIcons = { + topup: { + path: "M20 8V5H5a2 2 0 0 1 0-4h13v4 M3 3v16a2 2 0 0 0 2 2h15V8H5 M16 12v5 M13.5 14.5h5", + color: "green", + }, + transfer: { path: "M3 7h18l-4-4 M21 17H3l4 4", color: "blue" }, + refund: { path: "M8 4L3 9l5 5 M3 9h11a7 7 0 0 1 0 14", color: "teal" }, +}; + +function bankOperationAppearance( + kind: BankOperation["kind"], + categoryName?: string | null, +) { + const name = categoryName + ?.trim() + .toLocaleLowerCase("ru-RU") + .replaceAll("ё", "е"); + return kind === "expense" + ? name && Object.hasOwn(categoryIcons, name) + ? categoryIcons[name] + : otherIcon + : operationIcons[kind]; +} + +export { bankOperationAppearance }; diff --git a/src/entities/bank-card/ui/BankCardForm.test.tsx b/src/entities/bank-card/ui/BankCardForm.test.tsx index 14ed802..98ce96c 100644 --- a/src/entities/bank-card/ui/BankCardForm.test.tsx +++ b/src/entities/bank-card/ui/BankCardForm.test.tsx @@ -67,7 +67,7 @@ describe("BankCardForm", () => { it("retains an invalid opening date draft and explains conflict with saved history", async () => { const close = setup(card); fireEvent.change(screen.getByLabelText(/Дата начала/), { - target: { value: "2026-08-01" }, + target: { value: "01.08.2026" }, }); server.use( http.patch("*/api/v1/bank-cards/a", () => @@ -81,7 +81,7 @@ describe("BankCardForm", () => { await screen.findByText( "Дата начала противоречит сохранённой истории карты.", ); - expect(screen.getByLabelText(/Дата начала/)).toHaveValue("2026-08-01"); + expect(screen.getByLabelText(/Дата начала/)).toHaveValue("01.08.2026"); expect(close).not.toHaveBeenCalled(); }); it("keeps card edit version until explicit refresh adoption", async () => { diff --git a/src/entities/bank-card/ui/BankCardForm.tsx b/src/entities/bank-card/ui/BankCardForm.tsx index 1c45e8e..883e637 100644 --- a/src/entities/bank-card/ui/BankCardForm.tsx +++ b/src/entities/bank-card/ui/BankCardForm.tsx @@ -3,6 +3,7 @@ import { Button, Group, Modal, Stack, Text } from "@mantine/core"; import { type FC, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { formatDate } from "@/shared/lib"; import { useBankMutations } from "../api/mutations"; import { type BankCard, useBankCard } from "../api/queries"; import { @@ -110,7 +111,7 @@ const BankCardForm: FC = (props) => { <> {t("bank.versionHint", { - details: `${latest.name} · ${latest.openingBalance.value} RUB · ${latest.openingDate}`, + details: `${latest.name} · ${latest.openingBalance.value} RUB · ${formatDate(latest.openingDate)}`, })}