All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m57s
323 lines
9.0 KiB
TypeScript
323 lines
9.0 KiB
TypeScript
"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<string[]>(["all"]);
|
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([
|
|
"all",
|
|
]);
|
|
const [showReconciled, setShowReconciled] = useState<string>("all");
|
|
const [period, setPeriod] = useLocalStorage<Period>("transactions-period", "3months");
|
|
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]);
|
|
|
|
// 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<string>(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<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,
|
|
};
|
|
}
|