refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
+30 -4
View File
@@ -17,6 +17,7 @@ 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();
@@ -25,32 +26,46 @@ function useCardPageVm() {
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 } = useBankMutations();
const [archiveError, setArchiveError] = useState<unknown>();
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>(
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 });
if (item.kind === "transfer")
title = t(isOutgoing ? "bank.transferTo" : "bank.transferFrom", {
name: isOutgoing ? item.targetCardName : item.cardName,
});
return {
color: isOutgoing ? "red" : "green",
editable:
@@ -64,24 +79,34 @@ function useCardPageVm() {
metadata: [formatDate(item.date), item.categoryName, item.description]
.filter(Boolean)
.join(" · "),
onDelete: () => setDeleting(item),
onEdit: () => setOperation(item),
otherCard:
item.kind === "transfer"
? otherCardId
: /* eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. */ null,
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
title,
tone: isOutgoing ? ("negative" as const) : ("positive" as const),
value: `${isOutgoing ? "−" : "+"}${formatMoney(item.amount)}`,
};
});
return {
archiveError,
archivePending: updateCard.isPending,
card,
cards,
closeDeletion: () => setDeleting(null),
closeEditing: () => setEditingCard(false),
closeOperation: () => setOperation(null),
deleting,
editableOperation: operation === "new" ? undefined : (operation ?? undefined),
editingCard,
history,
notFound: isApiErrorCode(apiErrorCode(card.error), ApiErrorCode.BankNotFound),
notFound: isApiErrorCode(
apiErrorCode(card.error),
ApiErrorCode.BankNotFound,
),
operation,
page,
pageSize,
@@ -104,6 +129,7 @@ function useCardPageVm() {
toggleArchive: async () => {
if (!card.data) return;
setArchiveError(undefined);
try {
await updateCard.mutateAsync({
body: { archived: !card.data.archivedAt, version: card.data.version },