feat: add manual bank card screens

Add cards, operations, refunds, categories and monthly summaries with Mantine date controls.

Derive banking types from runtime OpenAPI. Cover recovery and session isolation.
This commit is contained in:
2026-09-12 18:34:41 +03:00
parent 427ca23a43
commit ff4e88edbe
50 changed files with 7795 additions and 10 deletions
+103
View File
@@ -0,0 +1,103 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams, useSearchParams } from "react-router";
import {
type BankOperation,
bankMonth,
useBankCard,
useBankCards,
useBankMutations,
useBankOperations,
useBankSummary,
} from "@/entities/bank-card";
import { apiErrorCode } from "@/shared/api";
import { formatMoney } from "@/shared/lib";
function useCardPageVm() {
const { cardId } = useParams();
const [params, setParams] = useSearchParams();
const { t } = useTranslation();
const month = bankMonth(params.get("month"));
const rawPage = Number(params.get("page") ?? 1);
const page = Number.isSafeInteger(rawPage) && rawPage > 0 ? rawPage : 1;
const card = useBankCard(cardId);
const { updateCard } = useBankMutations();
const [archiveError, setArchiveError] = useState<unknown>();
const cards = useBankCards(true);
const history = useBankOperations(cardId, { month, page });
const summary = useBankSummary(month, cardId);
const [editingCard, setEditingCard] = useState(false);
const [operation, setOperation] = useState<BankOperation | "new" | null>(
null,
);
const [deleting, setDeleting] = useState<BankOperation | null>(null);
const rows = history.data?.items.map((item) => {
const outgoing =
item.kind === "expense" ||
(item.kind === "transfer" && item.cardId === cardId);
return {
item,
title:
item.kind === "transfer"
? t(outgoing ? "bank.transferTo" : "bank.transferFrom", {
name: outgoing ? item.targetCardName : item.cardName,
})
: t(`bank.kinds.${item.kind}`),
metadata: [item.date, item.categoryName, item.description]
.filter(Boolean)
.join(" · "),
value: `${outgoing ? "−" : "+"}${formatMoney(item.amount)}`,
tone: outgoing ? ("negative" as const) : ("positive" as const),
otherCard:
item.kind === "transfer"
? outgoing
? item.targetCardId
: item.cardId
: null,
editable:
Boolean(cards.data) &&
!cards.data?.some(
(c) =>
(c.id === item.cardId || c.id === item.targetCardId) &&
c.archivedAt,
),
};
});
return {
card,
cards,
archiveError,
archivePending: updateCard.isPending,
toggleArchive: async () => {
if (!card.data) return;
setArchiveError(undefined);
try {
await updateCard.mutateAsync({
id: card.data.id,
body: { version: card.data.version, archived: !card.data.archivedAt },
});
} catch (failure) {
setArchiveError(failure);
}
},
history,
summary,
month,
page,
rows,
editingCard,
setEditingCard,
operation,
setOperation,
deleting,
setDeleting,
notFound: apiErrorCode(card.error) === "BANK_NOT_FOUND",
setMonth: (value: string) => {
if (value) setParams({ month: bankMonth(value) });
},
setPage: (value: number) => setParams({ month, page: String(value) }),
};
}
type CardPageVm = ReturnType<typeof useCardPageVm>;
export { type CardPageVm, useCardPageVm };