'use client' import { useState } from 'react' import Link from 'next/link' import { usePathname, useSearchParams, useRouter } from 'next/navigation' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Pencil, ChevronLeft, ChevronRight, Trash2, Tag, StickyNote, X, ArrowLeftRight } from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Textarea } from '@/components/ui/textarea' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { formatCents } from '@/lib/utils/currency' import { EditTransactionDialog } from './EditTransactionDialog' import { cn } from '@/lib/utils' export type TransactionRow = { id: string date: string description: string amountCents: number type: string category: string | null notes: string | null accountId: string budgetId: string | null account: { name: string; type: string } budget: { id: string; name: string; color: string | null } | null } type BudgetOption = { id: string; name: string; color: string | null } interface Props { transactions: TransactionRow[] total: number page: number limit: number showAccount?: boolean budgets: BudgetOption[] } export function TransactionTable({ transactions, total, page, limit, showAccount = true, budgets, }: Props) { const pathname = usePathname() const searchParams = useSearchParams() const router = useRouter() const [editing, setEditing] = useState(null) const [selected, setSelected] = useState>(new Set()) const [bulkDialog, setBulkDialog] = useState<'delete' | 'budget' | 'notes' | 'transfer' | null>(null) const [budgetTarget, setBudgetTarget] = useState('__none__') const [notesValue, setNotesValue] = useState('') const [working, setWorking] = useState(false) const totalPages = Math.ceil(total / limit) const allIds = transactions.map((t) => t.id) const allSelected = allIds.length > 0 && allIds.every((id) => selected.has(id)) const someSelected = selected.size > 0 function toggleAll() { if (allSelected) { setSelected((prev) => { const next = new Set(prev) allIds.forEach((id) => next.delete(id)) return next }) } else { setSelected((prev) => new Set([...prev, ...allIds])) } } function toggleOne(id: string) { setSelected((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } function clearSelection() { setSelected(new Set()) } function pageHref(p: number) { const params = new URLSearchParams(searchParams.toString()) params.set('page', String(p)) return `${pathname}?${params.toString()}` } async function bulkPost(body: object) { setWorking(true) const res = await fetch('/api/transactions/bulk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) setWorking(false) if (res.ok) { clearSelection() setBulkDialog(null) router.refresh() } } async function handleDelete() { await bulkPost({ action: 'delete', ids: [...selected] }) } async function handleAssignBudget() { await bulkPost({ action: 'assignBudget', ids: [...selected], budgetId: budgetTarget === '__none__' ? null : budgetTarget, }) } async function handleAddNotes() { await bulkPost({ action: 'addNotes', ids: [...selected], notes: notesValue }) } async function handleMarkTransfer() { await bulkPost({ action: 'markTransfer', ids: [...selected] }) } if (transactions.length === 0 && !someSelected) { return (
No transactions found.
) } const colSpan = showAccount ? 8 : 7 return ( <> {/* Bulk action bar */} {someSelected && (
{selected.size} selected
)}
Date Description {showAccount && Account} Amount Category Budget {transactions.map((tx) => { const isCredit = tx.type === 'CREDIT' const isTransfer = tx.type === 'TRANSFER' const isSelected = selected.has(tx.id) const dateStr = new Date(tx.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }) return ( toggleOne(tx.id)} aria-label="Select transaction" /> {dateStr}
{tx.description}
{tx.notes && (
{tx.notes}
)}
{showAccount && ( {tx.account.name} )} {isTransfer ? '⇄ ' : isCredit ? '+' : '-'}{formatCents(tx.amountCents)} {tx.category ?? '—'} {tx.budget ? ( {tx.budget.color && ( )} {tx.budget.name} ) : ( )}
) })}
{/* Pagination */}

{total.toLocaleString()} transaction{total !== 1 ? 's' : ''} {totalPages > 1 && ` · page ${page} of ${totalPages}`}

{totalPages > 1 && (
{page > 1 ? ( Prev ) : ( Prev )} {page < totalPages ? ( Next ) : ( Next )}
)}
{ if (!open) setEditing(null) }} budgets={budgets} /> {/* Delete confirm dialog */} { if (!o) setBulkDialog(null) }}> Delete {selected.size} transaction{selected.size !== 1 ? 's' : ''}?

This cannot be undone.

{/* Assign budget dialog */} { if (!o) setBulkDialog(null) }}> Assign budget

Apply to {selected.size} selected transaction{selected.size !== 1 ? 's' : ''}.

{/* Mark as transfer dialog */} { if (!o) setBulkDialog(null) }}> Mark as transfer?

{selected.size} transaction{selected.size !== 1 ? 's' : ''} will be marked as transfers and excluded from cash flow calculations.

{/* Add notes dialog */} { if (!o) setBulkDialog(null) }}> Set notes

Apply to {selected.size} selected transaction{selected.size !== 1 ? 's' : ''}. Leave blank to clear.