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 { Filter, Search, TrendingUp, TrendingDown, Calendar, RefreshCw, AlertCircle, X, Eye, ChevronUp, ChevronDown, } from "lucide-react"; import { apiClient } from "../lib/api"; import { formatCurrency, formatDate } from "../lib/utils"; import LoadingSpinner from "./LoadingSpinner"; import RawTransactionModal from "./RawTransactionModal"; import type { Account, Transaction, ApiResponse } from "../types/api"; export default function TransactionsTable() { const [searchTerm, setSearchTerm] = useState(""); const [selectedAccount, setSelectedAccount] = useState(""); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [minAmount, setMinAmount] = useState(""); const [maxAmount, setMaxAmount] = useState(""); const [showFilters, setShowFilters] = useState(false); 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(searchTerm); // Table state (remove pagination from table) const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); // Debounce search term to prevent excessive API calls useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 300); // 300ms delay return () => clearTimeout(timer); }, [searchTerm]); // Reset pagination when search term changes useEffect(() => { if (debouncedSearchTerm !== searchTerm) { setCurrentPage(1); } }, [debouncedSearchTerm, searchTerm]); const { data: accounts } = useQuery({ queryKey: ["accounts"], queryFn: apiClient.getAccounts, }); const { data: transactionsResponse, isLoading: transactionsLoading, error: transactionsError, refetch: refetchTransactions, } = useQuery>({ queryKey: [ "transactions", selectedAccount, startDate, endDate, currentPage, perPage, debouncedSearchTerm, ], queryFn: () => apiClient.getTransactions({ accountId: selectedAccount || undefined, startDate: startDate || undefined, endDate: 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 = searchTerm !== debouncedSearchTerm; // Reset pagination when total becomes 0 (no results) useEffect(() => { if (pagination && pagination.total === 0 && currentPage > 1) { setCurrentPage(1); } }, [pagination, currentPage]); const clearFilters = () => { setSearchTerm(""); setSelectedAccount(""); setStartDate(""); setEndDate(""); setMinAmount(""); setMaxAmount(""); setColumnFilters([]); setCurrentPage(1); // Reset to first page when clearing filters }; const setQuickDateFilter = (days: number) => { const endDate = new Date(); const startDate = new Date(); startDate.setDate(endDate.getDate() - days); setStartDate(startDate.toISOString().split("T")[0]); setEndDate(endDate.toISOString().split("T")[0]); setCurrentPage(1); // Reset to first page when changing date filters }; const setThisMonthFilter = () => { const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); setStartDate(startOfMonth.toISOString().split("T")[0]); setEndDate(endOfMonth.toISOString().split("T")[0]); setCurrentPage(1); // Reset to first page when changing date filters }; // Reset pagination when account filter changes useEffect(() => { setCurrentPage(1); }, [selectedAccount]); // Reset pagination when date filters change useEffect(() => { setCurrentPage(1); }, [startDate, endDate]); const handleViewRaw = (transaction: Transaction) => { setSelectedTransaction(transaction); setShowRawModal(true); }; const handleCloseModal = () => { setShowRawModal(false); setSelectedTransaction(null); }; const hasActiveFilters = searchTerm || selectedAccount || startDate || endDate || minAmount || maxAmount; // 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.name || "Unnamed Account"} •{" "} {account.institution_id}

)} {(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: searchTerm, }, onGlobalFilterChange: setSearchTerm, 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 (
{/* Filters */}

Transactions

{hasActiveFilters && ( )}
{showFilters && (
{/* Quick Date Filters */}
{/* Search */}
setSearchTerm(e.target.value)} placeholder="Description, name, reference..." className="pl-10 pr-3 py-2 w-full border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" /> {isSearchLoading && (
)}
{/* Account Filter */}
{/* Start Date */}
setStartDate(e.target.value)} className="pl-10 pr-3 py-2 w-full border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
{/* End Date */}
setEndDate(e.target.value)} className="pl-10 pr-3 py-2 w-full border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
{/* Amount Range Filters */}
setMinAmount(e.target.value)} placeholder="0.00" step="0.01" className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
setMaxAmount(e.target.value)} placeholder="1000.00" step="0.01" className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
)} {/* Results Summary */}

Showing {transactions.length} transaction {transactions.length !== 1 ? "s" : ""} ( {pagination ? ( <> {(pagination.page - 1) * pagination.per_page + 1}- {Math.min( pagination.page * pagination.per_page, pagination.total, )}{" "} of {pagination.total} ) : ( "loading..." )} ) {selectedAccount && accounts && ( for {accounts.find((acc) => acc.id === selectedAccount)?.name} )}

{/* 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.name || "Unnamed Account"} •{" "} {account.institution_id}

)} {(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 && (
{/* Mobile pagination controls */}
{/* Mobile pagination info */}

Page {pagination.page} of{" "} {pagination.total_pages}
Showing {(pagination.page - 1) * pagination.per_page + 1}- {Math.min(pagination.page * pagination.per_page, pagination.total)} of {pagination.total}

{/* Desktop pagination */}

Showing{" "} {(pagination.page - 1) * pagination.per_page + 1} {" "} to{" "} {Math.min( pagination.page * pagination.per_page, pagination.total, )} {" "} of {pagination.total}{" "} results

Page {pagination.page}{" "} of{" "} {pagination.total_pages}
)}
{/* Raw Transaction Modal */}
); }