Files
frontend/src/pages/card/ui/CardPage/CardPage.vm.ts
T
ksv741 e38ecd87d6
Frontend verify / commits (pull_request) Successful in 39s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Failing after 5m50s
fix(auth,bank): guard switch-auth nav and surface banking retry
AuthForm disables the switch-auth Link while submit is pending so pointer
click and Enter can no longer leave the auth route and lose the draft/error
context. The form restores a true Link after the request settles.

CardsPage/CardPage distinguish cold vs cached banking failures:
- cold card/section errors expose accessible alert + retry
- cached failures keep the previous data and offer a section GET retry
- summary-only/history-only outages do not hide successful sections
- notFound still skips the retry CTA

Tests cover the pending link guard, cold/cached cards error with retry,
summary failure with retry, and the prerequisite card list recovery.

Closes #56, #57
2026-10-14 10:43:34 +03:00

167 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams, useSearchParams } from "react-router";
import {
type BankOperation,
bankPeriod,
useBankCard,
useBankCards,
useBankOperations,
useBankSummary,
useUpdateBankCard,
} from "@/entities/bank-card";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
import { formatDate, formatMoney } from "@/shared/lib";
const PAGE_SIZES = new Set(["5", "10", "20", "50", "100", "all"]);
type CardPageVm = ReturnType<typeof useCardPageVm>;
function useCardPageVm() {
const { cardId } = useParams();
const [parameters, setParameters] = useSearchParams();
const { t } = useTranslation();
const period = bankPeriod(parameters);
const requestedSize = parameters.get("limit") ?? "10";
const pageSize = PAGE_SIZES.has(requestedSize) ? requestedSize : "10";
const rawPage = Number(parameters.get("page") ?? 1);
const page
= pageSize !== "all" && Number.isSafeInteger(rawPage) && rawPage > 0
? rawPage
: 1;
const card = useBankCard(cardId);
const updateCard = useUpdateBankCard();
const cards = useBankCards(true);
const history = useBankOperations(cardId, {
...period,
limit: pageSize === "all" ? "all" : Number(pageSize),
page,
});
const summary = useBankSummary(period, cardId);
const [editingCard, setEditingCard] = useState(false);
const [operation, setOperation] = useState<"new"
| BankOperation
| null>(
null,
);
const [deleting, setDeleting] = useState<BankOperation | null>(null);
const rows = history.data?.items.map((item) => {
const isOutgoing
= item.kind === "expense"
|| (item.kind === "transfer" && item.cardId === cardId);
const otherCardId = isOutgoing ? item.targetCardId : item.cardId;
let title = t(`bank.kinds.${item.kind}`);
if (item.kind === "transfer")
title = t(isOutgoing ? "bank.transferTo" : "bank.transferFrom", {
name: isOutgoing ? item.targetCardName : item.cardName,
});
return {
color: isOutgoing ? "red" : "green",
editable:
!history.isPlaceholderData
&& !card.data?.archivedAt
&& Boolean(cards.data)
&& !cards.data?.some(
c =>
(c.id === item.cardId || c.id === item.targetCardId)
&& c.archivedAt,
),
item,
metadata: [formatDate(item.date), item.categoryName, item.description]
.filter(Boolean)
.join(" · "),
onDelete: () => setDeleting(item),
onEdit: () => setOperation(item),
otherCard:
item.kind === "transfer"
? otherCardId
: null,
title,
tone: isOutgoing ? ("negative" as const) : ("positive" as const),
value: `${isOutgoing ? "−" : "+"}${formatMoney(item.amount)}`,
};
});
return {
archiveError: updateCard.isError ? updateCard.error : undefined,
archivePending: updateCard.isPending,
canAddOperation: !card.data?.archivedAt && Boolean(cards.data),
card: card.data,
cardError: card.error,
cardLoading: card.isLoading,
cardRefreshError: card.isError,
cardRefreshing: card.isFetching,
cardRetry: () => card.refetch(),
cards: cards.data,
cardsFeedback: cards.isError
? { cached: Boolean(cards.data), error: cards.error }
: undefined,
cardsRetry: () => cards.refetch(),
closeDeletion: () => setDeleting(null),
closeEditing: () => setEditingCard(false),
closeOperation: () => setOperation(null),
deleting,
editableOperation: operation === "new" ? undefined : (operation ?? undefined),
editingCard,
history: {
changingPage: history.isPlaceholderData,
error: history.error,
loading: history.isLoading,
page: history.data?.page,
refetch: () => history.refetch(),
},
notFound: isApiErrorCode(
apiErrorCode(card.error),
ApiErrorCode.BankNotFound,
),
openCardEditor: () => setEditingCard(true),
openNewOperation: () => setOperation("new"),
operation,
page,
pageSize,
period,
rows,
setPage: (value: number) =>
setParameters({ ...period, limit: pageSize, page: String(value) }),
setPageSize: (value: null | string) => {
if (value && PAGE_SIZES.has(value))
setParameters({ ...period, limit: value });
},
setPeriod: (value: [null | string, null | string]) => {
if (value[0] && value[1])
setParameters({ from: value[0], limit: pageSize, to: value[1] });
},
summary: {
data: summary.data,
error: summary.error,
fetching: summary.isFetching,
loading: summary.isLoading,
refetch: () => summary.refetch(),
},
toggleArchive: () => {
if (!card.data) return;
updateCard.mutate({
body: { archived: !card.data.archivedAt, version: card.data.version },
id: card.data.id,
});
},
totalPages:
pageSize === "all"
? 1
: Math.max(1, Math.ceil((history.data?.total ?? 0) / Number(pageSize))),
};
}
export { type CardPageVm, useCardPageVm };