"use client"; import { useState, useMemo, useEffect, useCallback } from "react"; import { useSearchParams } from "next/navigation"; import { useBankingMetadata, useTransactions, useDuplicateIds, } from "@/lib/hooks"; import { useLocalStorage } from "@/hooks/use-local-storage"; import type { TransactionsPaginatedParams } from "@/services/banking.service"; import type { Category } from "@/lib/types"; 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(["all"]); const [selectedCategories, setSelectedCategories] = useState([ "all", ]); const [showReconciled, setShowReconciled] = useState("all"); const [period, setPeriod] = useLocalStorage("transactions-period", "3months"); const [customStartDate, setCustomStartDate] = useState( undefined, ); const [customEndDate, setCustomEndDate] = useState( undefined, ); const [isCustomDatePickerOpen, setIsCustomDatePickerOpen] = useState(false); const [showDuplicates, setShowDuplicates] = useState(false); // Pagination state const [page, setPage] = useState(0); // Sort state const [sortField, setSortField] = useState("date"); const [sortOrder, setSortOrder] = useState("desc"); // Selection state const [selectedTransactions, setSelectedTransactions] = useState>( 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]); // Handle categoryIds and includeUncategorized from URL params useEffect(() => { const categoryIdsParam = searchParams.get("categoryIds"); const includeUncategorizedParam = searchParams.get("includeUncategorized"); if (categoryIdsParam && metadata) { const categoryIds = categoryIdsParam.split(","); // Expand parent categories to include their children const expandedCategoryIds = new Set(categoryIds); categoryIds.forEach((categoryId) => { // Check if this is a parent category const category = metadata.categories.find( (c: Category) => c.id === categoryId ); if (category && category.parentId === null) { // Find all children of this parent const children = metadata.categories.filter( (c: Category) => c.parentId === categoryId ); children.forEach((child: Category) => { expandedCategoryIds.add(child.id); }); } }); setSelectedCategories(Array.from(expandedCategoryIds)); setPage(0); } else if (includeUncategorizedParam === "true") { setSelectedCategories(["uncategorized"]); setPage(0); } }, [searchParams, metadata]); // 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() } = 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, }; }