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:
@@ -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 };
|
||||
Reference in New Issue
Block a user