refactor: streamline transaction page logic by consolidating state management and enhancing pagination, improving overall performance and maintainability

This commit is contained in:
Julien Froidefond
2025-12-08 12:52:59 +01:00
parent ba4d112cb8
commit 53bae084c4
7 changed files with 926 additions and 560 deletions

View File

@@ -0,0 +1,286 @@
"use client";
import { useState, useMemo, useEffect, useCallback } from "react";
import { useSearchParams } from "next/navigation";
import {
useBankingMetadata,
useTransactions,
useDuplicateIds,
} from "@/lib/hooks";
import type { TransactionsPaginatedParams } from "@/services/banking.service";
type SortField = "date" | "amount" | "description";
type SortOrder = "asc" | "desc";
type Period = "1month" | "3months" | "6months" | "12months" | "custom" | "all";
const PAGE_SIZE = 100;
export function useTransactionsPage() {
const searchParams = useSearchParams();
const { data: metadata, isLoading: isLoadingMetadata } = useBankingMetadata();
// Search state
const [searchQuery, setSearchQuery] = useState("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState("");
// Filter state
const [selectedAccounts, setSelectedAccounts] = useState<string[]>(["all"]);
const [selectedCategories, setSelectedCategories] = useState<string[]>([
"all",
]);
const [showReconciled, setShowReconciled] = useState<string>("all");
const [period, setPeriod] = useState<Period>("all");
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
undefined
);
const [customEndDate, setCustomEndDate] = useState<Date | undefined>(
undefined
);
const [isCustomDatePickerOpen, setIsCustomDatePickerOpen] = useState(false);
const [showDuplicates, setShowDuplicates] = useState(false);
// Pagination state
const [page, setPage] = useState(0);
// Sort state
const [sortField, setSortField] = useState<SortField>("date");
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
// Selection state
const [selectedTransactions, setSelectedTransactions] = useState<Set<string>>(
new Set()
);
// Debounce search query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
setPage(0);
}, 300);
return () => clearTimeout(timer);
}, [searchQuery]);
// Handle accountId from URL params
useEffect(() => {
const accountId = searchParams.get("accountId");
if (accountId) {
setSelectedAccounts([accountId]);
setPage(0);
}
}, [searchParams]);
// Calculate start date based on period
const startDate = useMemo(() => {
const now = new Date();
switch (period) {
case "1month":
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
case "3months":
return new Date(now.getFullYear(), now.getMonth() - 3, 1);
case "6months":
return new Date(now.getFullYear(), now.getMonth() - 6, 1);
case "12months":
return new Date(now.getFullYear(), now.getMonth() - 12, 1);
case "custom":
return customStartDate || new Date(0);
default:
return new Date(0);
}
}, [period, customStartDate]);
// Calculate end date (only for custom period)
const endDate = useMemo(() => {
if (period === "custom" && customEndDate) {
return customEndDate;
}
return undefined;
}, [period, customEndDate]);
// Build transaction query params
const transactionParams = useMemo(() => {
const params: TransactionsPaginatedParams = {
limit: PAGE_SIZE,
offset: page * PAGE_SIZE,
sortField,
sortOrder,
};
if (startDate && period !== "all") {
params.startDate = startDate.toISOString().split("T")[0];
}
if (endDate) {
params.endDate = endDate.toISOString().split("T")[0];
}
if (!selectedAccounts.includes("all")) {
params.accountIds = selectedAccounts;
}
if (!selectedCategories.includes("all")) {
if (selectedCategories.includes("uncategorized")) {
params.includeUncategorized = true;
} else {
params.categoryIds = selectedCategories;
}
}
if (debouncedSearchQuery) {
params.search = debouncedSearchQuery;
}
if (showReconciled !== "all") {
params.isReconciled = showReconciled === "reconciled";
}
return params;
}, [
page,
startDate,
endDate,
selectedAccounts,
selectedCategories,
debouncedSearchQuery,
showReconciled,
sortField,
sortOrder,
period,
]);
// Fetch transactions
const {
data: transactionsData,
isLoading: isLoadingTransactions,
invalidate: invalidateTransactions,
} = useTransactions(transactionParams, !!metadata);
// Fetch duplicate IDs
const { data: duplicateIds = new Set<string>() } = useDuplicateIds();
// Handlers
const handleAccountsChange = useCallback((accounts: string[]) => {
setSelectedAccounts(accounts);
setPage(0);
}, []);
const handleCategoriesChange = useCallback((categories: string[]) => {
setSelectedCategories(categories);
setPage(0);
}, []);
const handleReconciledChange = useCallback((value: string) => {
setShowReconciled(value);
setPage(0);
}, []);
const handlePeriodChange = useCallback(
(p: Period) => {
setPeriod(p);
setPage(0);
if (p !== "custom") {
setIsCustomDatePickerOpen(false);
} else {
setIsCustomDatePickerOpen(true);
}
},
[]
);
const handleCustomStartDateChange = useCallback((date: Date | undefined) => {
setCustomStartDate(date);
setPage(0);
}, []);
const handleCustomEndDateChange = useCallback((date: Date | undefined) => {
setCustomEndDate(date);
setPage(0);
}, []);
const handleSortChange = useCallback((field: SortField) => {
if (sortField === field) {
setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
} else {
setSortField(field);
setSortOrder(field === "date" ? "desc" : "asc");
}
setPage(0);
}, [sortField]);
const toggleSelectAll = useCallback(() => {
if (!transactionsData) return;
if (selectedTransactions.size === transactionsData.transactions.length) {
setSelectedTransactions(new Set());
} else {
setSelectedTransactions(
new Set(transactionsData.transactions.map((t) => t.id))
);
}
}, [transactionsData, selectedTransactions.size]);
const toggleSelectTransaction = useCallback((id: string) => {
setSelectedTransactions((prev) => {
const newSelected = new Set(prev);
if (newSelected.has(id)) {
newSelected.delete(id);
} else {
newSelected.add(id);
}
return newSelected;
});
}, []);
const handlePageChange = useCallback((newPage: number) => {
setPage(newPage);
}, []);
const clearSelection = useCallback(() => {
setSelectedTransactions(new Set());
}, []);
return {
// Metadata
metadata,
isLoadingMetadata,
// Search
searchQuery,
setSearchQuery,
// Filters
selectedAccounts,
onAccountsChange: handleAccountsChange,
selectedCategories,
onCategoriesChange: handleCategoriesChange,
showReconciled,
onReconciledChange: handleReconciledChange,
period,
onPeriodChange: handlePeriodChange,
customStartDate,
customEndDate,
onCustomStartDateChange: handleCustomStartDateChange,
onCustomEndDateChange: handleCustomEndDateChange,
isCustomDatePickerOpen,
onCustomDatePickerOpenChange: setIsCustomDatePickerOpen,
showDuplicates,
onShowDuplicatesChange: setShowDuplicates,
// Pagination
page,
pageSize: PAGE_SIZE,
onPageChange: handlePageChange,
// Sort
sortField,
sortOrder,
onSortChange: handleSortChange,
// Selection
selectedTransactions,
onToggleSelectAll: toggleSelectAll,
onToggleSelectTransaction: toggleSelectTransaction,
clearSelection,
// Data
transactionsData,
isLoadingTransactions,
invalidateTransactions,
duplicateIds,
transactionParams,
};
}