first build commit
This commit is contained in:
185
src/components/transactions/TransactionTable.tsx
Normal file
185
src/components/transactions/TransactionTable.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Pencil, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
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 [editing, setEditing] = useState<TransactionRow | null>(null)
|
||||
const totalPages = Math.ceil(total / limit)
|
||||
|
||||
function pageHref(p: number) {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.set('page', String(p))
|
||||
return `${pathname}?${params.toString()}`
|
||||
}
|
||||
|
||||
if (transactions.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed p-12 text-center text-muted-foreground">
|
||||
No transactions found.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-md border overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-28">Date</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
{showAccount && <TableHead className="w-36">Account</TableHead>}
|
||||
<TableHead className="w-32 text-right">Amount</TableHead>
|
||||
<TableHead className="w-32">Category</TableHead>
|
||||
<TableHead className="w-32">Budget</TableHead>
|
||||
<TableHead className="w-10" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{transactions.map((tx) => {
|
||||
const isCredit = tx.type === 'CREDIT'
|
||||
const dateStr = new Date(tx.date).toLocaleDateString('en-US', {
|
||||
month: 'short', day: 'numeric', year: 'numeric',
|
||||
})
|
||||
return (
|
||||
<TableRow key={tx.id}>
|
||||
<TableCell className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{dateStr}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs">
|
||||
<div className="truncate text-sm">{tx.description}</div>
|
||||
{tx.notes && (
|
||||
<div className="truncate text-xs text-muted-foreground">{tx.notes}</div>
|
||||
)}
|
||||
</TableCell>
|
||||
{showAccount && (
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{tx.account.name}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-right tabular-nums font-medium">
|
||||
<span className={cn('text-sm', isCredit ? 'text-green-600' : '')}>
|
||||
{isCredit ? '+' : '-'}{formatCents(tx.amountCents)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{tx.category ?? '—'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{tx.budget ? (
|
||||
<span className="flex items-center gap-1.5 text-sm">
|
||||
{tx.budget.color && (
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full shrink-0"
|
||||
style={{ backgroundColor: tx.budget.color }}
|
||||
/>
|
||||
)}
|
||||
{tx.budget.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<button
|
||||
onClick={() => setEditing(tx)}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent transition-colors"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="sr-only">Edit</span>
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{total.toLocaleString()} transaction{total !== 1 ? 's' : ''}
|
||||
{totalPages > 1 && ` · page ${page} of ${totalPages}`}
|
||||
</p>
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center gap-1">
|
||||
{page > 1 ? (
|
||||
<Link
|
||||
href={pageHref(page - 1)}
|
||||
className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />Prev
|
||||
</Link>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium opacity-50 cursor-not-allowed">
|
||||
<ChevronLeft className="h-4 w-4" />Prev
|
||||
</span>
|
||||
)}
|
||||
{page < totalPages ? (
|
||||
<Link
|
||||
href={pageHref(page + 1)}
|
||||
className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors"
|
||||
>
|
||||
Next<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium opacity-50 cursor-not-allowed">
|
||||
Next<ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<EditTransactionDialog
|
||||
transaction={editing}
|
||||
onOpenChange={(open) => { if (!open) setEditing(null) }}
|
||||
budgets={budgets}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user