import { useAtomValue, useSetAtom } from "jotai" import { MissingFiltersBanner } from "./MissingFiltersBanner" import { bankRecDateAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms" import { Paragraph } from "@/components/ui/typography" import { formatDate } from "@/lib/date" import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view" import { formatCurrency, getCurrencyFormatInfo } from "@/lib/numbers" import { getCompanyCurrency } from "@/lib/company" import { ArrowDownRight, ArrowUpRight, CheckCircle2, ChevronDown, DollarSign, ExternalLink, ImportIcon, ListIcon, Search, Undo2, XCircle } from "lucide-react" import ErrorBanner from "@/components/ui/error-banner" import { Badge } from "@/components/ui/badge" import { useGetBankTransactions } from "./utils" import { BankTransaction } from "@/types/Accounts/BankTransaction" import { Button } from "@/components/ui/button" import _ from "@/lib/translate" import { Input } from "@/components/ui/input" import CurrencyInput from "react-currency-input-field" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { getCurrencySymbol } from "@/lib/currency" import { useDebounceValue } from "usehooks-ts" import type { ColumnDef } from "@tanstack/react-table" import { useCallback, useMemo, useState } from "react" import { Link } from "react-router" import { Empty, EmptyTitle, EmptyHeader, EmptyMedia, EmptyDescription, EmptyContent } from "@/components/ui/empty" import { InputGroup, InputGroupAddon } from "@/components/ui/input-group" const BankTransactions = () => { const selectedBank = useAtomValue(selectedBankAccountAtom) const dates = useAtomValue(bankRecDateAtom) if (!selectedBank || !dates) { return } return <> } const BankTransactionListView = () => { const { data, error } = useGetBankTransactions() const bankAccount = useAtomValue(selectedBankAccountAtom) const dates = useAtomValue(bankRecDateAtom) const formattedFromDate = formatDate(dates.fromDate) const formattedToDate = formatDate(dates.toDate) const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom) const onUndo = useCallback( (transaction: BankTransaction) => { setBankRecUnreconcileModalAtom(transaction.name) }, [setBankRecUnreconcileModalAtom], ) const accountCurrency = useMemo( () => bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ""), [bankAccount?.account_currency, bankAccount?.company], ) const transactionColumns = useMemo[]>( () => [ { accessorKey: "date", header: _("Date"), size: 112, meta: { tabularNums: true } satisfies ListViewColumnMeta, cell: ({ row }) => formatDate(row.original.date), }, { accessorKey: "description", header: _("Description"), size: 250, // meta: { gridWidth: "minmax(0,2fr)" } satisfies ListViewColumnMeta, cell: ({ row }) => row.original.description, }, { accessorKey: "reference_number", header: _("Reference #"), size: 128, cell: ({ row }) => row.original.reference_number, }, { accessorKey: "withdrawal", header: _("Withdrawal"), size: 120, meta: { align: "right" } satisfies ListViewColumnMeta, cell: ({ row }) => {formatCurrency(row.original.withdrawal, accountCurrency)}, }, { accessorKey: "deposit", header: _("Deposit"), size: 120, meta: { align: "right" } satisfies ListViewColumnMeta, cell: ({ row }) => {formatCurrency(row.original.deposit, accountCurrency)}, }, { accessorKey: "unallocated_amount", header: _("Unallocated"), size: 120, meta: { align: "right" } satisfies ListViewColumnMeta, cell: ({ row }) => {formatCurrency(row.original.unallocated_amount, accountCurrency)}, }, { accessorKey: "transaction_type", header: _("Type"), size: 112, cell: ({ row }) => row.original.transaction_type ? {row.original.transaction_type} : null, }, { id: "status", header: _("Status"), size: 168, meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta, cell: ({ row }) => { const tx = row.original if (!tx.allocated_amount || (tx.allocated_amount && tx.allocated_amount === 0)) { return ( {_("Not Reconciled")} ) } if (tx.allocated_amount && tx.allocated_amount > 0 && tx.unallocated_amount !== 0) { return ( {_("Partially Reconciled")} ) } return ( {_("Reconciled")} ) }, }, { id: "actions", header: _("Actions"), size: 200, enableResizing: false, meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta, cell: ({ row }) => (
{row.original.allocated_amount && row.original.allocated_amount > 0 ? ( ) : null}
), }, ], [_, accountCurrency, onUndo], ) const [search, setSearch] = useDebounceValue('', 250) const [amountFilter, setAmountFilter] = useState<{ value: number, stringValue?: string | number }>({ value: 0, stringValue: '0.00' }) const [typeFilter, setTypeFilter] = useState('All') const [status, setStatus] = useState<'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled'>('All') const onSearchChange = (e: React.ChangeEvent) => { setSearch(e.target.value) } const filteredResults = useMemo(() => { if (!data) { return [] } return data.message.filter((transaction) => { if (search && !transaction.description?.toLowerCase().includes(search.toLowerCase())) { return false } if (typeFilter !== 'All') { if (typeFilter === 'Debits' && transaction.deposit && transaction.deposit > 0) { return false } if (typeFilter === 'Credits' && transaction.withdrawal && transaction.withdrawal > 0) { return false } } if (status !== 'All') { if (status === 'Reconciled' && transaction.status !== 'Reconciled') { return false } if (status === 'Unreconciled') { if (transaction.status === 'Reconciled') { return false } // Filter out partially reconciled transactions if (transaction.allocated_amount && transaction.allocated_amount > 0 && transaction.unallocated_amount !== 0) { return false } } if (status === 'Partially Reconciled') { if (transaction.status === 'Reconciled') { return false } if ((transaction.allocated_amount ?? 0) === 0) { return false } } } if (amountFilter.value > 0 && transaction.withdrawal !== amountFilter.value && transaction.deposit !== amountFilter.value) { return false } return true }) }, [data, search, amountFilter, typeFilter, status]) return
${bankAccount?.account_name}`, `${formattedFromDate}`, `${formattedToDate}`]) }} />
{error && } row.name} maxHeight="calc(100vh - 200px)" scrollAreaClassName="min-h-[calc(100vh-200px)]" emptyState={ {_("No bank transactions found")} {_("There are no transactions in the system for the selected bank account and dates that match the filters.")} {data && data.message.length === 0 ? : null} } />
} interface FilterProps { onSearchChange: (e: React.ChangeEvent) => void search: string results: BankTransaction[] setAmountFilter: (value: { value: number, stringValue?: string | number }) => void amountFilter: { value: number, stringValue?: string | number } onTypeFilterChange: (type: string) => void typeFilter: string status: 'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled' setStatus: (status: 'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled') => void } const Filters = ({ onSearchChange, search, results, setAmountFilter, amountFilter, onTypeFilterChange, typeFilter, status, setStatus, }: FilterProps) => { const bankAccount = useAtomValue(selectedBankAccountAtom) const currency = bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '') const currencySymbol = getCurrencySymbol(currency) const formatInfo = getCurrencyFormatInfo(currency) const groupSeparator = formatInfo.group_sep || "," const decimalSeparator = formatInfo.decimal_str || "." return
{results?.length} {_(results?.length === 1 ? "result" : "results")}
{ // If the input ends with a decimal or a decimal with trailing zeroes, store the string since we need the user to be able to type the decimals. // When the user eventually types the decimals or blurs out, the value is formatted anyway. // Otherwise store the float value // Check if the value ends with a decimal or a decimal with trailing zeroes const isDecimal = v?.endsWith(decimalSeparator) || v?.endsWith(decimalSeparator + '0') const newValue = isDecimal ? v : values?.float ?? '' setAmountFilter({ value: Number(newValue), stringValue: newValue }) }} // @ts-expect-error - CurrencyInputProps doesn't have a variant prop but Input does variant={"outline"} customInput={Input} />
onTypeFilterChange('All')}> {_("All")} onTypeFilterChange('Debits')}> {_("Debits")} onTypeFilterChange('Credits')}> {_("Credits")}
setStatus('All')}>{} {_("All")} setStatus('Reconciled')}>{} {_("Reconciled")} setStatus('Unreconciled')}>{} {_("Unreconciled")} setStatus('Partially Reconciled')}>{} {_("Partially Reconciled")}
} export default BankTransactions