import { useState, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, flexRender, } from "@tanstack/react-table"; import type { ColumnDef, SortingState, ColumnFiltersState, } from "@tanstack/react-table"; import { TrendingUp, TrendingDown, RefreshCw, AlertCircle, Eye, ChevronUp, ChevronDown, } from "lucide-react"; import { apiClient } from "../lib/api"; import { formatCurrency, formatDate } from "../lib/utils"; import TransactionSkeleton from "./TransactionSkeleton"; import FiltersSkeleton from "./FiltersSkeleton"; import RawTransactionModal from "./RawTransactionModal"; import { FilterBar, type FilterState } from "./filters"; import { DataTablePagination } from "./ui/data-table-pagination"; import { Card } from "./ui/card"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { Button } from "./ui/button"; import type { Account, Transaction, ApiResponse } from "../types/api"; export default function TransactionsTable() { // Filter state consolidated into a single object const [filterState, setFilterState] = useState({ searchTerm: "", selectedAccount: "", startDate: "", endDate: "", }); const [showRawModal, setShowRawModal] = useState(false); const [selectedTransaction, setSelectedTransaction] = useState(null); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [perPage, setPerPage] = useState(50); // Debounced search state const [debouncedSearchTerm, setDebouncedSearchTerm] = useState( filterState.searchTerm, ); // Table state (remove pagination from table) const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); // Helper function to update filter state const handleFilterChange = (key: keyof FilterState, value: string) => { setFilterState((prev) => ({ ...prev, [key]: value })); }; // Helper function to clear all filters const handleClearFilters = () => { setFilterState({ searchTerm: "", selectedAccount: "", startDate: "", endDate: "", }); setColumnFilters([]); setCurrentPage(1); }; // Debounce search term to prevent excessive API calls useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(filterState.searchTerm); }, 300); // 300ms delay return () => clearTimeout(timer); }, [filterState.searchTerm]); // Reset pagination when search term changes useEffect(() => { if (debouncedSearchTerm !== filterState.searchTerm) { setCurrentPage(1); } }, [debouncedSearchTerm, filterState.searchTerm]); const { data: accounts } = useQuery({ queryKey: ["accounts"], queryFn: apiClient.getAccounts, }); const { data: transactionsResponse, isLoading: transactionsLoading, error: transactionsError, refetch: refetchTransactions, } = useQuery>({ queryKey: [ "transactions", filterState.selectedAccount, filterState.startDate, filterState.endDate, currentPage, perPage, debouncedSearchTerm, ], queryFn: () => apiClient.getTransactions({ accountId: filterState.selectedAccount || undefined, startDate: filterState.startDate || undefined, endDate: filterState.endDate || undefined, page: currentPage, perPage: perPage, search: debouncedSearchTerm || undefined, summaryOnly: false, }), }); const transactions = transactionsResponse?.data || []; const pagination = transactionsResponse?.pagination; // Check if search is currently debouncing const isSearchLoading = filterState.searchTerm !== debouncedSearchTerm; // Reset pagination when total becomes 0 (no results) useEffect(() => { if (pagination && pagination.total === 0 && currentPage > 1) { setCurrentPage(1); } }, [pagination, currentPage]); // Reset pagination when filters change useEffect(() => { setCurrentPage(1); }, [filterState.selectedAccount, filterState.startDate, filterState.endDate]); const handleViewRaw = (transaction: Transaction) => { setSelectedTransaction(transaction); setShowRawModal(true); }; const handleCloseModal = () => { setShowRawModal(false); setSelectedTransaction(null); }; const hasActiveFilters = filterState.searchTerm || filterState.selectedAccount || filterState.startDate || filterState.endDate; // Define columns const columns: ColumnDef[] = [ { accessorKey: "description", header: "Description", cell: ({ row }) => { const transaction = row.original; const account = accounts?.find( (acc) => acc.id === transaction.account_id, ); const isPositive = transaction.transaction_value > 0; return (
{isPositive ? ( ) : ( )}

{transaction.description}

{account && (

{account.display_name || "Unnamed Account"}

)} {(transaction.creditor_name || transaction.debtor_name) && (

{isPositive ? "From: " : "To: "} {transaction.creditor_name || transaction.debtor_name}

)} {transaction.reference && (

Ref: {transaction.reference}

)}
); }, }, { accessorKey: "transaction_value", header: "Amount", cell: ({ row }) => { const transaction = row.original; const isPositive = transaction.transaction_value > 0; return (

{isPositive ? "+" : ""} {formatCurrency( transaction.transaction_value, transaction.transaction_currency, )}

); }, sortingFn: "basic", }, { accessorKey: "transaction_date", header: "Date", cell: ({ row }) => { const transaction = row.original; return (
{transaction.transaction_date ? formatDate(transaction.transaction_date) : "No date"} {transaction.booking_date && transaction.booking_date !== transaction.transaction_date && (

Booked: {formatDate(transaction.booking_date)}

)}
); }, sortingFn: "datetime", }, { id: "actions", header: "", cell: ({ row }) => { const transaction = row.original; return ( ); }, }, ]; const table = useReactTable({ data: transactions, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, state: { sorting, columnFilters, globalFilter: filterState.searchTerm, }, onGlobalFilterChange: (value: string) => handleFilterChange("searchTerm", value), globalFilterFn: (row, _columnId, filterValue) => { // Custom global filter that searches multiple fields const transaction = row.original; const searchLower = filterValue.toLowerCase(); const description = transaction.description || ""; const creditorName = transaction.creditor_name || ""; const debtorName = transaction.debtor_name || ""; const reference = transaction.reference || ""; return ( description.toLowerCase().includes(searchLower) || creditorName.toLowerCase().includes(searchLower) || debtorName.toLowerCase().includes(searchLower) || reference.toLowerCase().includes(searchLower) ); }, }); if (transactionsLoading) { return (
); } if (transactionsError) { return ( Failed to load transactions

Unable to fetch transactions from the Leggen API.

); } return (
{/* New FilterBar */} {/* Responsive Table/Cards */} {/* Desktop Table View (hidden on mobile) */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {table.getRowModel().rows.length === 0 ? ( ) : ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} )) )}
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} {header.column.getCanSort() && (
)}

No transactions found

{hasActiveFilters ? "Try adjusting your filters to see more results." : "No transactions are available for the selected criteria."}

{flexRender( cell.column.columnDef.cell, cell.getContext(), )}
{/* Mobile Card View (visible only on mobile) */}
{table.getRowModel().rows.length === 0 ? (

No transactions found

{hasActiveFilters ? "Try adjusting your filters to see more results." : "No transactions are available for the selected criteria."}

) : (
{table.getRowModel().rows.map((row) => { const transaction = row.original; const account = accounts?.find( (acc) => acc.id === transaction.account_id, ); const isPositive = transaction.transaction_value > 0; return (
{isPositive ? ( ) : ( )}

{transaction.description}

{account && (

{account.display_name || "Unnamed Account"}

)} {(transaction.creditor_name || transaction.debtor_name) && (

{isPositive ? "From: " : "To: "} {transaction.creditor_name || transaction.debtor_name}

)} {transaction.reference && (

Ref: {transaction.reference}

)}

{transaction.transaction_date ? formatDate(transaction.transaction_date) : "No date"} {transaction.booking_date && transaction.booking_date !== transaction.transaction_date && ( (Booked:{" "} {formatDate(transaction.booking_date)}) )}

{isPositive ? "+" : ""} {formatCurrency( transaction.transaction_value, transaction.transaction_currency, )}

); })}
)}
{/* Pagination */} {pagination && ( )}
{/* Raw Transaction Modal */}
); }