chore: save pending changes
This commit is contained in:
@@ -22,6 +22,7 @@ const card: BankCard = {
|
|||||||
name: "Daily",
|
name: "Daily",
|
||||||
bankName: null,
|
bankName: null,
|
||||||
lastFour: null,
|
lastFour: null,
|
||||||
|
paymentSystem: null,
|
||||||
openingBalance: { currency: "RUB", value: "100" },
|
openingBalance: { currency: "RUB", value: "100" },
|
||||||
openingDate: "2026-09-01",
|
openingDate: "2026-09-01",
|
||||||
balance: { currency: "RUB", value: "100" },
|
balance: { currency: "RUB", value: "100" },
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export {
|
|||||||
moneyCents,
|
moneyCents,
|
||||||
positiveMoneySchema,
|
positiveMoneySchema,
|
||||||
} from "./model/form-validation";
|
} from "./model/form-validation";
|
||||||
|
export { BankCardFace } from "./ui/BankCardFace";
|
||||||
export { BankCardForm } from "./ui/BankCardForm";
|
export { BankCardForm } from "./ui/BankCardForm";
|
||||||
export { BankFeedback } from "./ui/BankFeedback";
|
export { BankFeedback } from "./ui/BankFeedback";
|
||||||
export { BankOperationIcon } from "./ui/BankOperationIcon";
|
export { BankOperationIcon } from "./ui/BankOperationIcon";
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ const bankCardSchema = z.object({
|
|||||||
(value) => !value || /^\d{4}$/.test(value),
|
(value) => !value || /^\d{4}$/.test(value),
|
||||||
validationMessage("bank.validation.lastFour"),
|
validationMessage("bank.validation.lastFour"),
|
||||||
),
|
),
|
||||||
|
paymentSystem: z
|
||||||
|
.enum(["", "mir", "visa", "mastercard"], {
|
||||||
|
errorMap: validationMessage("bank.validation.paymentSystem"),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
openingBalance: moneySchema,
|
openingBalance: moneySchema,
|
||||||
openingDate: bankDateSchema,
|
openingDate: bankDateSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
.face {
|
||||||
|
--card-base: #242b35;
|
||||||
|
--card-light: #485666;
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 28rem;
|
||||||
|
min-height: 15rem;
|
||||||
|
padding: clamp(1.1rem, 3vw, 1.6rem);
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(125deg, var(--card-light), var(--card-base) 75%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 #ffffff40,
|
||||||
|
0 8px 18px #18233118;
|
||||||
|
}
|
||||||
|
.face::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
width: 24rem;
|
||||||
|
height: 24rem;
|
||||||
|
border: 1px solid #ffffff20;
|
||||||
|
border-radius: 50%;
|
||||||
|
right: -13rem;
|
||||||
|
top: -9rem;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2.5rem #ffffff08,
|
||||||
|
0 0 0 5rem #ffffff06;
|
||||||
|
}
|
||||||
|
.face[data-tone="green"] {
|
||||||
|
--card-base: #07543c;
|
||||||
|
--card-light: #21844a;
|
||||||
|
}
|
||||||
|
.face[data-tone="yellow"] {
|
||||||
|
--card-base: #edbe26;
|
||||||
|
--card-light: #ffe478;
|
||||||
|
color: #252316;
|
||||||
|
}
|
||||||
|
.face[data-tone="red"] {
|
||||||
|
--card-base: #980f28;
|
||||||
|
--card-light: #d63244;
|
||||||
|
}
|
||||||
|
.face[data-tone="blue"] {
|
||||||
|
--card-base: #073f86;
|
||||||
|
--card-light: #226db8;
|
||||||
|
}
|
||||||
|
.top,
|
||||||
|
.bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.bank {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 750;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.archive {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
border-radius: 2rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.hardware {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
margin: 1.4rem 0 1.1rem;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
display: block;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 1.9rem;
|
||||||
|
border: 1px solid #a99054;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 30%,
|
||||||
|
#9b875a 31%,
|
||||||
|
transparent 33%,
|
||||||
|
transparent 66%,
|
||||||
|
#9b875a 67%,
|
||||||
|
transparent 69%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
0deg,
|
||||||
|
transparent 32%,
|
||||||
|
#9b875a 33%,
|
||||||
|
transparent 36%,
|
||||||
|
transparent 65%,
|
||||||
|
#9b875a 66%,
|
||||||
|
transparent 69%
|
||||||
|
),
|
||||||
|
linear-gradient(135deg, #e5d6a6, #bfaa75 50%, #f1e6c4);
|
||||||
|
}
|
||||||
|
.number {
|
||||||
|
font-family: var(--mantine-font-family-monospace);
|
||||||
|
font-size: clamp(1rem, 2.3vw, 1.35rem);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.balance {
|
||||||
|
font-size: clamp(1.2rem, 2.5vw, 1.55rem);
|
||||||
|
font-weight: 650;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 850;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
.network[data-network="mastercard"] {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.circles {
|
||||||
|
display: block;
|
||||||
|
width: 3.3rem;
|
||||||
|
height: 2rem;
|
||||||
|
margin: 0 auto 0.15rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.circles::before,
|
||||||
|
.circles::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.circles::before {
|
||||||
|
left: 0;
|
||||||
|
background: #eb302b;
|
||||||
|
}
|
||||||
|
.circles::after {
|
||||||
|
right: 0;
|
||||||
|
background: #ffad19;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { SimpleGrid } from "@mantine/core";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { BankCardFace } from "./BankCardFace";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Product/BankCards/CardAppearance",
|
||||||
|
component: BankCardFace,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: {
|
||||||
|
card: {
|
||||||
|
id: "fictional",
|
||||||
|
name: "Для повседневных покупок",
|
||||||
|
bankName: "Сбербанк",
|
||||||
|
lastFour: "4829",
|
||||||
|
paymentSystem: "mir",
|
||||||
|
openingBalance: { value: "0", currency: "RUB" },
|
||||||
|
openingDate: "2026-01-01",
|
||||||
|
balance: { value: "128450.75", currency: "RUB" },
|
||||||
|
archivedAt: null,
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof BankCardFace>;
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
export const Banks: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2 }} maw={920}>
|
||||||
|
{["Сбербанк", "Т-Банк", "Альфа-Банк", "ВТБ", null].map(
|
||||||
|
(bankName, index) => (
|
||||||
|
<BankCardFace
|
||||||
|
key={bankName ?? "unknown"}
|
||||||
|
card={{
|
||||||
|
...args.card,
|
||||||
|
bankName,
|
||||||
|
paymentSystem:
|
||||||
|
(["mir", "mastercard", "visa", "mir", null] as const)[index] ??
|
||||||
|
null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SimpleGrid>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
export const MissingDetails: Story = {
|
||||||
|
args: {
|
||||||
|
card: {
|
||||||
|
...meta.args.card,
|
||||||
|
bankName: null,
|
||||||
|
lastFour: null,
|
||||||
|
paymentSystem: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const LongArchived: Story = {
|
||||||
|
args: {
|
||||||
|
card: {
|
||||||
|
...meta.args.card,
|
||||||
|
name: "Карта с очень длинным пользовательским названием для повседневных покупок",
|
||||||
|
bankName: "Банк с очень длинным названием без пробелов".repeat(2),
|
||||||
|
archivedAt: "2026-09-12",
|
||||||
|
paymentSystem: "mastercard",
|
||||||
|
balance: { value: "184467440737095516.14", currency: "RUB" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import type { FC } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { formatMoney } from "@/shared/lib";
|
||||||
|
import type { BankCard } from "../api/queries";
|
||||||
|
import classes from "./BankCardFace.module.css";
|
||||||
|
|
||||||
|
type BankCardFaceProps = { card: BankCard };
|
||||||
|
const BankCardFace: FC<BankCardFaceProps> = (props) => {
|
||||||
|
const { card } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const bank = card.bankName?.toLocaleLowerCase("ru") ?? "";
|
||||||
|
const tone = /сбер|sber/.test(bank)
|
||||||
|
? "green"
|
||||||
|
: /тинькофф|т[ -]?банк|t[ -]?bank|tinkoff/.test(bank)
|
||||||
|
? "yellow"
|
||||||
|
: /альфа|alfa/.test(bank)
|
||||||
|
? "red"
|
||||||
|
: /втб|vtb|газпром|gazprom/.test(bank)
|
||||||
|
? "blue"
|
||||||
|
: "graphite";
|
||||||
|
return (
|
||||||
|
<div className={classes.face} data-tone={tone}>
|
||||||
|
<div className={classes.top}>
|
||||||
|
<span className={classes.bank}>{card.bankName || t("bank.card")}</span>
|
||||||
|
{card.archivedAt && (
|
||||||
|
<span className={classes.archive}>{t("bank.archived")}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={classes.hardware} aria-hidden="true">
|
||||||
|
<span className={classes.chip} />
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
width="24"
|
||||||
|
height="30"
|
||||||
|
viewBox="0 0 24 30"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 11a8 8 0 0 1 0 8m5-12a15 15 0 0 1 0 16m5-20a23 23 0 0 1 0 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classes.number}
|
||||||
|
>{`•••• •••• •••• ${card.lastFour || "••••"}`}</div>
|
||||||
|
<div className={classes.bottom}>
|
||||||
|
<div className={classes.details}>
|
||||||
|
<div className={classes.name}>{card.name}</div>
|
||||||
|
<div className={classes.balance}>{formatMoney(card.balance)}</div>
|
||||||
|
</div>
|
||||||
|
{card.paymentSystem && (
|
||||||
|
<span className={classes.network} data-network={card.paymentSystem}>
|
||||||
|
{card.paymentSystem === "mastercard" && (
|
||||||
|
<span className={classes.circles} aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
{t(`bank.paymentSystems.${card.paymentSystem}`)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BankCardFace };
|
||||||
@@ -14,6 +14,7 @@ const card: BankCard = {
|
|||||||
name: "Основная",
|
name: "Основная",
|
||||||
bankName: "Банк",
|
bankName: "Банк",
|
||||||
lastFour: "1234",
|
lastFour: "1234",
|
||||||
|
paymentSystem: null,
|
||||||
openingBalance: { currency: "RUB", value: "100" },
|
openingBalance: { currency: "RUB", value: "100" },
|
||||||
balance: { currency: "RUB", value: "50" },
|
balance: { currency: "RUB", value: "50" },
|
||||||
openingDate: "2026-01-01",
|
openingDate: "2026-01-01",
|
||||||
@@ -62,8 +63,30 @@ describe("BankCardForm", () => {
|
|||||||
name: "Основная",
|
name: "Основная",
|
||||||
bankName: null,
|
bankName: null,
|
||||||
lastFour: null,
|
lastFour: null,
|
||||||
|
paymentSystem: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it("loads and saves the selected payment system", async () => {
|
||||||
|
const close = setup({ ...card, paymentSystem: "mir" });
|
||||||
|
expect(
|
||||||
|
screen.getByRole("combobox", { name: /Платёжная система/ }),
|
||||||
|
).toHaveValue("МИР");
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("combobox", { name: /Платёжная система/ }),
|
||||||
|
);
|
||||||
|
fireEvent.click(await screen.findByRole("option", { name: "Visa" }));
|
||||||
|
let body: unknown;
|
||||||
|
server.use(
|
||||||
|
http.patch("*/api/v1/bank-cards/a", async ({ request }) => {
|
||||||
|
body = await request.json();
|
||||||
|
return HttpResponse.json({ ...card, paymentSystem: "visa" });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
||||||
|
await waitFor(() => expect(close).toHaveBeenCalled());
|
||||||
|
expect(body).toMatchObject({ paymentSystem: "visa" });
|
||||||
|
});
|
||||||
|
|
||||||
it("retains an invalid opening date draft and explains conflict with saved history", async () => {
|
it("retains an invalid opening date draft and explains conflict with saved history", async () => {
|
||||||
const close = setup(card);
|
const close = setup(card);
|
||||||
fireEvent.change(screen.getByLabelText(/Дата начала/), {
|
fireEvent.change(screen.getByLabelText(/Дата начала/), {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const BankCardForm: FC<BankCardFormProps> = (props) => {
|
|||||||
name: props.card?.name ?? "",
|
name: props.card?.name ?? "",
|
||||||
bankName: props.card?.bankName ?? "",
|
bankName: props.card?.bankName ?? "",
|
||||||
lastFour: props.card?.lastFour ?? "",
|
lastFour: props.card?.lastFour ?? "",
|
||||||
|
paymentSystem: props.card?.paymentSystem ?? "",
|
||||||
openingBalance: props.card?.openingBalance.value ?? "0",
|
openingBalance: props.card?.openingBalance.value ?? "0",
|
||||||
openingDate: props.card?.openingDate ?? bankToday(),
|
openingDate: props.card?.openingDate ?? bankToday(),
|
||||||
},
|
},
|
||||||
@@ -57,6 +58,7 @@ const BankCardForm: FC<BankCardFormProps> = (props) => {
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
bankName: values.bankName || null,
|
bankName: values.bankName || null,
|
||||||
lastFour: values.lastFour || null,
|
lastFour: values.lastFour || null,
|
||||||
|
paymentSystem: values.paymentSystem || null,
|
||||||
};
|
};
|
||||||
const financial = {
|
const financial = {
|
||||||
openingBalance: {
|
openingBalance: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Stack, Text, TextInput } from "@mantine/core";
|
import { Select, Stack, Text, TextInput } from "@mantine/core";
|
||||||
import { DateInput } from "@mantine/dates";
|
import { DateInput } from "@mantine/dates";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
@@ -37,6 +37,26 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
|
|||||||
{...register("lastFour")}
|
{...register("lastFour")}
|
||||||
error={errors.lastFour?.message}
|
error={errors.lastFour?.message}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="paymentSystem"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
value={field.value || null}
|
||||||
|
onChange={(value) => field.onChange(value ?? "")}
|
||||||
|
label={t("bank.paymentSystem")}
|
||||||
|
placeholder={t("bank.paymentSystemUnknown")}
|
||||||
|
clearable
|
||||||
|
data={["mir", "visa", "mastercard"].map((value) => ({
|
||||||
|
value,
|
||||||
|
label: t(`bank.paymentSystems.${value}`),
|
||||||
|
}))}
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
|
error={errors.paymentSystem?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("bank.openingBalance")}
|
label={t("bank.openingBalance")}
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Anchor,
|
Anchor,
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -15,11 +14,11 @@ import type { FC } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import {
|
import {
|
||||||
|
BankCardFace,
|
||||||
BankFeedback,
|
BankFeedback,
|
||||||
BankOperationIcon,
|
BankOperationIcon,
|
||||||
BankSummaryView,
|
BankSummaryView,
|
||||||
} from "@/entities/bank-card";
|
} from "@/entities/bank-card";
|
||||||
import { formatMoney } from "@/shared/lib";
|
|
||||||
import { EntryCard, LoadingSkeleton, StateView } from "@/shared/ui";
|
import { EntryCard, LoadingSkeleton, StateView } from "@/shared/ui";
|
||||||
import classes from "./CardPage.module.css";
|
import classes from "./CardPage.module.css";
|
||||||
import type { CardPageVm } from "./CardPage.vm";
|
import type { CardPageVm } from "./CardPage.vm";
|
||||||
@@ -65,21 +64,10 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Paper p="md" withBorder>
|
<Paper p="md" withBorder>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group justify="space-between">
|
|
||||||
<Title order={1} className={classes.wrap}>
|
<Title order={1} className={classes.wrap}>
|
||||||
{card.name}
|
{card.name}
|
||||||
</Title>
|
</Title>
|
||||||
{card.archivedAt && (
|
<BankCardFace card={card} />
|
||||||
<Badge color="gray">{t("bank.archived")}</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Text c="dimmed">
|
|
||||||
{card.bankName}
|
|
||||||
{card.lastFour ? ` · •• ${card.lastFour}` : ""}
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={700} className={classes.wrap}>
|
|
||||||
{formatMoney(card.balance)}
|
|
||||||
</Text>
|
|
||||||
<Text c="dimmed">{t("bank.manualBalance")}</Text>
|
<Text c="dimmed">{t("bank.manualBalance")}</Text>
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const card: BankCard = {
|
|||||||
name: "Повседневная",
|
name: "Повседневная",
|
||||||
bankName: null,
|
bankName: null,
|
||||||
lastFour: null,
|
lastFour: null,
|
||||||
|
paymentSystem: null,
|
||||||
balance: money("0"),
|
balance: money("0"),
|
||||||
openingBalance: money("0"),
|
openingBalance: money("0"),
|
||||||
openingDate: "2026-01-01",
|
openingDate: "2026-01-01",
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-width: 28rem;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background: transparent;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
.card:focus-visible {
|
.card:focus-visible {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -14,8 +13,11 @@ import { MonthPickerInput } from "@mantine/dates";
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { BankFeedback, BankSummaryView } from "@/entities/bank-card";
|
import {
|
||||||
import { formatMoney } from "@/shared/lib";
|
BankCardFace,
|
||||||
|
BankFeedback,
|
||||||
|
BankSummaryView,
|
||||||
|
} from "@/entities/bank-card";
|
||||||
import { LoadingSkeleton, StateView } from "@/shared/ui";
|
import { LoadingSkeleton, StateView } from "@/shared/ui";
|
||||||
import classes from "./CardsPage.module.css";
|
import classes from "./CardsPage.module.css";
|
||||||
import type { CardsPageVm } from "./CardsPage.vm";
|
import type { CardsPageVm } from "./CardsPage.vm";
|
||||||
@@ -44,27 +46,10 @@ const CardsPageView: FC<CardsPageViewProps> = (props) => {
|
|||||||
key={card.id}
|
key={card.id}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={card.to}
|
to={card.to}
|
||||||
withBorder
|
p={0}
|
||||||
p="md"
|
|
||||||
className={classes.card}
|
className={classes.card}
|
||||||
>
|
>
|
||||||
<Stack gap="xs">
|
<BankCardFace card={card} />
|
||||||
<Group>
|
|
||||||
<Text fw={600} size="lg">
|
|
||||||
{card.name}
|
|
||||||
</Text>
|
|
||||||
{card.archivedAt && (
|
|
||||||
<Badge color="gray">{t("bank.archived")}</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Text c="dimmed">
|
|
||||||
{card.bankName}
|
|
||||||
{card.lastFour ? ` · •• ${card.lastFour}` : ""}
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={650}>
|
|
||||||
{formatMoney(card.balance)}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -82,8 +67,8 @@ const CardsPageView: FC<CardsPageViewProps> = (props) => {
|
|||||||
) : vm.cards.isLoading ? (
|
) : vm.cards.isLoading ? (
|
||||||
<LoadingSkeleton label={t("bank.loading")}>
|
<LoadingSkeleton label={t("bank.loading")}>
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }}>
|
<SimpleGrid cols={{ base: 1, sm: 2 }}>
|
||||||
<Skeleton h={120} />
|
<Skeleton h={250} />
|
||||||
<Skeleton h={120} />
|
<Skeleton h={250} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</LoadingSkeleton>
|
</LoadingSkeleton>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Vendored
+6
@@ -718,6 +718,8 @@ export interface components {
|
|||||||
name: string;
|
name: string;
|
||||||
bankName: string | null;
|
bankName: string | null;
|
||||||
lastFour: string | null;
|
lastFour: string | null;
|
||||||
|
/** @enum {string|null} */
|
||||||
|
paymentSystem: "mir" | "visa" | "mastercard" | null;
|
||||||
openingBalance: components["schemas"]["MoneyDto"];
|
openingBalance: components["schemas"]["MoneyDto"];
|
||||||
openingDate: string;
|
openingDate: string;
|
||||||
balance: components["schemas"]["MoneyDto"];
|
balance: components["schemas"]["MoneyDto"];
|
||||||
@@ -735,6 +737,8 @@ export interface components {
|
|||||||
name: string;
|
name: string;
|
||||||
bankName?: string | null;
|
bankName?: string | null;
|
||||||
lastFour?: string | null;
|
lastFour?: string | null;
|
||||||
|
/** @enum {string|null} */
|
||||||
|
paymentSystem?: "mir" | "visa" | "mastercard" | null;
|
||||||
openingBalance: components["schemas"]["RubMoneyInputDto"];
|
openingBalance: components["schemas"]["RubMoneyInputDto"];
|
||||||
/** Format: date */
|
/** Format: date */
|
||||||
openingDate: string;
|
openingDate: string;
|
||||||
@@ -830,6 +834,8 @@ export interface components {
|
|||||||
name?: string;
|
name?: string;
|
||||||
bankName?: string | null;
|
bankName?: string | null;
|
||||||
lastFour?: string | null;
|
lastFour?: string | null;
|
||||||
|
/** @enum {string|null} */
|
||||||
|
paymentSystem?: "mir" | "visa" | "mastercard" | null;
|
||||||
openingBalance?: components["schemas"]["RubMoneyInputDto"];
|
openingBalance?: components["schemas"]["RubMoneyInputDto"];
|
||||||
/** Format: date */
|
/** Format: date */
|
||||||
openingDate?: string;
|
openingDate?: string;
|
||||||
|
|||||||
@@ -4780,6 +4780,15 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"paymentSystem": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"mir",
|
||||||
|
"visa",
|
||||||
|
"mastercard"
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"openingBalance": {
|
"openingBalance": {
|
||||||
"$ref": "#/components/schemas/MoneyDto"
|
"$ref": "#/components/schemas/MoneyDto"
|
||||||
},
|
},
|
||||||
@@ -4802,6 +4811,7 @@
|
|||||||
"name",
|
"name",
|
||||||
"bankName",
|
"bankName",
|
||||||
"lastFour",
|
"lastFour",
|
||||||
|
"paymentSystem",
|
||||||
"openingBalance",
|
"openingBalance",
|
||||||
"openingDate",
|
"openingDate",
|
||||||
"balance",
|
"balance",
|
||||||
@@ -4850,6 +4860,15 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"pattern": "^[0-9]{4}$"
|
"pattern": "^[0-9]{4}$"
|
||||||
},
|
},
|
||||||
|
"paymentSystem": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"mir",
|
||||||
|
"visa",
|
||||||
|
"mastercard"
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"openingBalance": {
|
"openingBalance": {
|
||||||
"$ref": "#/components/schemas/RubMoneyInputDto"
|
"$ref": "#/components/schemas/RubMoneyInputDto"
|
||||||
},
|
},
|
||||||
@@ -5210,6 +5229,15 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"pattern": "^[0-9]{4}$"
|
"pattern": "^[0-9]{4}$"
|
||||||
},
|
},
|
||||||
|
"paymentSystem": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"mir",
|
||||||
|
"visa",
|
||||||
|
"mastercard"
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"openingBalance": {
|
"openingBalance": {
|
||||||
"$ref": "#/components/schemas/RubMoneyInputDto"
|
"$ref": "#/components/schemas/RubMoneyInputDto"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ const ru = {
|
|||||||
"Остаток до операций с выбранной даты. Валюта — рубли (RUB).",
|
"Остаток до операций с выбранной даты. Валюта — рубли (RUB).",
|
||||||
name: "Название карты",
|
name: "Название карты",
|
||||||
bankName: "Банк (необязательно)",
|
bankName: "Банк (необязательно)",
|
||||||
|
paymentSystem: "Платёжная система (необязательно)",
|
||||||
|
paymentSystemUnknown: "Не указана",
|
||||||
|
paymentSystems: { mir: "МИР", visa: "Visa", mastercard: "Mastercard" },
|
||||||
lastFour: "Последние четыре цифры (необязательно)",
|
lastFour: "Последние четыре цифры (необязательно)",
|
||||||
openingBalance: "Начальный остаток, ₽",
|
openingBalance: "Начальный остаток, ₽",
|
||||||
openingDate: "Дата начала учёта",
|
openingDate: "Дата начала учёта",
|
||||||
@@ -137,6 +140,7 @@ const ru = {
|
|||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
name: "Введите название от 1 до 100 символов.",
|
name: "Введите название от 1 до 100 символов.",
|
||||||
|
paymentSystem: "Выберите платёжную систему из списка.",
|
||||||
bankName: "Не более 100 символов.",
|
bankName: "Не более 100 символов.",
|
||||||
lastFour: "Укажите ровно четыре цифры или оставьте поле пустым.",
|
lastFour: "Укажите ровно четыре цифры или оставьте поле пустым.",
|
||||||
money:
|
money:
|
||||||
|
|||||||
Reference in New Issue
Block a user