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
139 lines
2.9 KiB
TypeScript
139 lines
2.9 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Anchor,
|
|
Group,
|
|
Skeleton,
|
|
Stack,
|
|
Text,
|
|
} from "@mantine/core";
|
|
import {
|
|
DeleteIcon,
|
|
EditIcon,
|
|
OperationIcon,
|
|
} from "@wealthledger/icons";
|
|
import { EntryCard, LoadingSkeleton } from "@wealthledger/ui";
|
|
import { type FC } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Link } from "react-router";
|
|
|
|
import {
|
|
BankFeedback,
|
|
bankOperationAppearance,
|
|
} from "@/entities/bank-card";
|
|
|
|
import type { CardPageVm } from "./CardPage.vm";
|
|
|
|
import classes from "./CardPage.module.css";
|
|
|
|
type CardHistoryProps = Pick<CardPageVm,
|
|
"history"
|
|
| "period"
|
|
| "rows"
|
|
>;
|
|
|
|
// eslint-disable-next-line sonarjs/function-return-type -- React renders elements, lists and empty output.
|
|
const CardHistory: FC<CardHistoryProps> = (props) => {
|
|
const vm = props;
|
|
const { t } = useTranslation();
|
|
|
|
if (vm.rows) {
|
|
if (vm.rows.length > 0) {
|
|
return vm.rows.map((row) => {
|
|
const details = row.otherCard
|
|
? (
|
|
<Anchor
|
|
component={Link}
|
|
to={`/cards/${row.otherCard}?from=${vm.period.from}&to=${vm.period.to}`}
|
|
>
|
|
{t("bank.card")}
|
|
</Anchor>
|
|
)
|
|
: undefined;
|
|
|
|
return (
|
|
<section
|
|
aria-label={`${row.title} ${row.item.date}`}
|
|
className={classes.operation}
|
|
key={row.item.id}
|
|
>
|
|
<EntryCard
|
|
actions={(
|
|
<Group gap={4} wrap="nowrap">
|
|
<ActionIcon
|
|
aria-label={t("bank.edit")}
|
|
disabled={!row.editable}
|
|
onClick={row.onEdit}
|
|
size="sm"
|
|
title={t("bank.edit")}
|
|
variant="subtle"
|
|
>
|
|
<EditIcon size={18} strokeWidth={1.5} />
|
|
</ActionIcon>
|
|
|
|
<ActionIcon
|
|
aria-label={t("bank.delete")}
|
|
color="red"
|
|
disabled={!row.editable}
|
|
onClick={row.onDelete}
|
|
size="sm"
|
|
title={t("bank.delete")}
|
|
variant="subtle"
|
|
>
|
|
<DeleteIcon size={18} strokeWidth={1.5} />
|
|
</ActionIcon>
|
|
</Group>
|
|
)}
|
|
color={row.color}
|
|
details={details}
|
|
icon={(
|
|
<OperationIcon
|
|
{...bankOperationAppearance(
|
|
row.item.kind,
|
|
row.item.categoryName,
|
|
)}
|
|
/>
|
|
)}
|
|
initial={row.title.charAt(0)}
|
|
metadata={row.metadata}
|
|
title={row.title}
|
|
tone={row.tone}
|
|
value={row.value}
|
|
valueLabel={t(`bank.kinds.${row.item.kind}`)}
|
|
wrap
|
|
/>
|
|
</section>
|
|
);
|
|
});
|
|
}
|
|
|
|
return <Text>{t("bank.noOperations")}</Text>;
|
|
}
|
|
|
|
if (vm.history.error) {
|
|
return (
|
|
<BankFeedback
|
|
cached={Boolean(vm.rows)}
|
|
error={vm.history.error}
|
|
loadingError={!vm.rows}
|
|
retry={vm.history.refetch}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (vm.history.loading) {
|
|
return (
|
|
<LoadingSkeleton label={t("bank.loadingHistory")}>
|
|
<Stack>
|
|
<Skeleton h={90} />
|
|
|
|
<Skeleton h={90} />
|
|
</Stack>
|
|
</LoadingSkeleton>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
export { CardHistory };
|