first build commit

This commit is contained in:
2026-04-19 00:44:43 -04:00
parent bc271b7ce1
commit 55debd082b
82 changed files with 6217 additions and 97 deletions

View 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}
/>
</>
)
}