import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLocation } from "react-router-dom"; import { Wand2, Tag, Receipt } from "lucide-react"; import { PageHelp } from "../components/shared/PageHelp"; import EmptyState from "../components/shared/EmptyState"; import OnboardingStepCard from "../components/onboarding/OnboardingStepCard"; import { useOnboarding } from "../contexts/OnboardingContext"; import { readOnboardingNavState, showsStepCard, transactionsCardModel, } from "../utils/onboardingPath"; import { useTransactions, showsEmptyState, GUIDED_ENTRY_FILTERS, } from "../hooks/useTransactions"; import TransactionFilterBar from "../components/transactions/TransactionFilterBar"; import TransactionSummaryBar from "../components/transactions/TransactionSummaryBar"; import TransactionTable from "../components/transactions/TransactionTable"; import TransactionPagination from "../components/transactions/TransactionPagination"; import ContextMenu from "../components/shared/ContextMenu"; import AddKeywordDialog from "../components/categories/AddKeywordDialog"; import { listAllLinkedTransfersForTooltip, type LinkedTransferTooltipRow, } from "../services/balance.service"; import type { TransactionRow } from "../shared/types"; export default function TransactionsPage() { const { t } = useTranslation(); const location = useLocation(); // The link of the "categorize by hand" step opens the guided entry (#399), // read like any navigation state through `readOnboardingNavState` (CWE-476). // The hook takes its start filters at mount only: the card consuming the // navigation state afterwards changes nothing. const { state, setFilter, setSort, setPage, updateCategory, saveNotes, autoCategorize, addKeywordToCategory, loadSplitChildren, saveSplit, deleteSplit } = useTransactions( readOnboardingNavState(location.state).guided ? GUIDED_ENTRY_FILTERS : undefined ); const onboarding = useOnboarding(); const [resultMessage, setResultMessage] = useState(null); const [menu, setMenu] = useState<{ x: number; y: number; row: TransactionRow } | null>(null); const [pending, setPending] = useState(null); // Issue #142 — single batch lookup for the inlined transfer icon. One // SELECT on mount gives us a Map the table consults via // `.has()` per row. Avoids an N+1 hit on the rendered page. const [linkedTransfersByTxId, setLinkedTransfersByTxId] = useState< Map >(new Map()); useEffect(() => { listAllLinkedTransfersForTooltip() .then(setLinkedTransfersByTxId) .catch(() => setLinkedTransfersByTxId(new Map())); }, []); const handleRowContextMenu = (e: React.MouseEvent, row: TransactionRow) => { e.preventDefault(); setMenu({ x: e.clientX, y: e.clientY, row }); }; const handleAutoCategorize = async () => { setResultMessage(null); const count = await autoCategorize(); if (count > 0) { setResultMessage(t("transactions.autoCategorizeResult", { count })); } else { setResultMessage(t("transactions.autoCategorizeNone")); } setTimeout(() => setResultMessage(null), 4000); }; // #353 — a profile without any transaction gets the "import first" card in // place of the filter bar, table and pagination. `rows.length === 0` is not // the signal: the default filter is "since January 1st", so an empty page // can still sit on a populated profile. const isEmptyProfile = showsEmptyState(state); // The getting-started card follows the list's filter (#399). Its target is // highlighted only while the card is on screen: a ring needs the card that // explains it. const cardModel = transactionsCardModel(state.filters, onboarding.checklist); const target = showsStepCard(onboarding.mode, onboarding.cardPage, location.pathname) ? cardModel.target : null; return (

{t("transactions.title")}

{resultMessage && ( {resultMessage} )}
{state.error && (
{state.error}
)} {isEmptyProfile ? ( } title={t("transactions.empty.title")} description={t("transactions.empty.description")} primaryAction={{ label: t("transactions.empty.cta"), to: "/import" }} /> ) : ( <> {state.isLoading ? (
{t("common.loading")}
) : ( <> )} )} {menu && ( setMenu(null)} items={[ { icon: , label: t("reports.keyword.addFromTransaction"), onClick: () => setPending(menu.row), }, ]} /> )} {pending && ( setPending(null)} onApplied={() => setPending(null)} /> )}
); }