import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, 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 } 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); // Table state const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); const { data: accounts } = useQuery({ queryKey: ["accounts"], queryFn: apiClient.getAccounts, }); const { data: transactions, isLoading: transactionsLoading, error: transactionsError, refetch: refetchTransactions, } = useQuery({ queryKey: ["transactions", selectedAccount, startDate, endDate], queryFn: () => apiClient.getTransactions({ accountId: selectedAccount || undefined, startDate: startDate || undefined, endDate: endDate || undefined, summaryOnly: false, }), }); const clearFilters = () => { setSearchTerm(""); setSelectedAccount(""); setStartDate(""); setEndDate(""); setMinAmount(""); setMaxAmount(""); setColumnFilters([]); }; 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]); }; 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]); }; 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(), getPaginationRowModel: getPaginationRowModel(), 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" />
{/* 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 {table.getFilteredRowModel().rows.length} transaction {table.getFilteredRowModel().rows.length !== 1 ? "s" : ""} {selectedAccount && accounts && ( for {accounts.find((acc) => acc.id === selectedAccount)?.name} )}

{/* Table */}
{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(), )}
{/* Pagination */}

Showing{" "} {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} {" "} to{" "} {Math.min( (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, table.getFilteredRowModel().rows.length, )} {" "} of{" "} {table.getFilteredRowModel().rows.length} {" "} results

Page{" "} {table.getState().pagination.pageIndex + 1} {" "} of{" "} {table.getPageCount()}
{/* Raw Transaction Modal */}
); }