"use client" import { BulkActionsMenu } from "@/components/transactions/bulk-actions" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { calcTotalPerCurrency } from "@/lib/stats" import { cn, formatCurrency } from "@/lib/utils" import { Category, Field, Project, Transaction } from "@prisma/client" import { formatDate } from "date-fns" import { ArrowDownIcon, ArrowUpIcon, File } from "lucide-react" import { useRouter, useSearchParams } from "next/navigation" import { useEffect, useMemo, useState } from "react" type FieldRenderer = { name: string code: string classes?: string sortable: boolean formatValue?: (transaction: Transaction & any) => React.ReactNode footerValue?: (transactions: Transaction[]) => React.ReactNode } type FieldWithRenderer = Field & { renderer: FieldRenderer } export const standardFieldRenderers: Record = { name: { name: "Name", code: "name", classes: "font-medium min-w-[120px] max-w-[300px] overflow-hidden", sortable: true, }, merchant: { name: "Merchant", code: "merchant", classes: "min-w-[120px] max-w-[250px] overflow-hidden", sortable: true, }, issuedAt: { name: "Date", code: "issuedAt", classes: "min-w-[100px]", sortable: true, formatValue: (transaction: Transaction) => transaction.issuedAt ? formatDate(transaction.issuedAt, "yyyy-MM-dd") : "", }, projectCode: { name: "Project", code: "projectCode", sortable: true, formatValue: (transaction: Transaction & { project: Project }) => transaction.projectCode ? ( {transaction.project?.name || ""} ) : ( "-" ), }, categoryCode: { name: "Category", code: "categoryCode", sortable: true, formatValue: (transaction: Transaction & { category: Category }) => transaction.categoryCode ? ( {transaction.category?.name || ""} ) : ( "-" ), }, files: { name: "Files", code: "files", sortable: false, formatValue: (transaction: Transaction) => (
{(transaction.files as string[]).length}
), }, total: { name: "Total", code: "total", classes: "text-right", sortable: true, formatValue: (transaction: Transaction) => (
{transaction.total && transaction.currencyCode ? formatCurrency(transaction.total, transaction.currencyCode) : transaction.total} {transaction.convertedTotal && transaction.convertedCurrencyCode && transaction.convertedCurrencyCode !== transaction.currencyCode && ( ({formatCurrency(transaction.convertedTotal, transaction.convertedCurrencyCode)}) )}
), footerValue: (transactions: Transaction[]) => { const totalPerCurrency = calcTotalPerCurrency(transactions) return (
{Object.entries(totalPerCurrency).map(([currency, total]) => (
{formatCurrency(total, currency)}
))}
) }, }, } const getFieldRenderer = (field: Field): FieldRenderer => { if (standardFieldRenderers[field.code as keyof typeof standardFieldRenderers]) { return standardFieldRenderers[field.code as keyof typeof standardFieldRenderers] } else { return { name: field.name, code: field.code, classes: "", sortable: false, } } } export function TransactionList({ transactions, fields = [] }: { transactions: Transaction[]; fields?: Field[] }) { const [selectedIds, setSelectedIds] = useState([]) const router = useRouter() const searchParams = useSearchParams() const [sorting, setSorting] = useState<{ field: string | null; direction: "asc" | "desc" | null }>(() => { const ordering = searchParams.get("ordering") if (!ordering) return { field: null, direction: null } const isDesc = ordering.startsWith("-") return { field: isDesc ? ordering.slice(1) : ordering, direction: isDesc ? "desc" : "asc", } }) const visibleFields = useMemo( (): FieldWithRenderer[] => fields .filter((field) => field.isVisibleInList) .map((field) => ({ ...field, renderer: getFieldRenderer(field), })), [fields] ) const toggleAllRows = () => { if (selectedIds.length === transactions.length) { setSelectedIds([]) } else { setSelectedIds(transactions.map((transaction) => transaction.id)) } } const toggleOneRow = (e: React.MouseEvent, id: string) => { e.stopPropagation() if (selectedIds.includes(id)) { setSelectedIds(selectedIds.filter((item) => item !== id)) } else { setSelectedIds([...selectedIds, id]) } } const handleRowClick = (id: string) => { router.push(`/transactions/${id}`) } const handleSort = (field: string) => { let newDirection: "asc" | "desc" | null = "asc" if (sorting.field === field) { if (sorting.direction === "asc") newDirection = "desc" else if (sorting.direction === "desc") newDirection = null } setSorting({ field: newDirection ? field : null, direction: newDirection, }) } const renderFieldInTable = (transaction: Transaction, field: FieldWithRenderer): string | React.ReactNode => { if (field.isExtra) { return transaction.extra?.[field.code as keyof typeof transaction.extra] ?? "" } else if (field.renderer.formatValue) { return field.renderer.formatValue(transaction) } else { return String(transaction[field.code as keyof Transaction]) } } useEffect(() => { const params = new URLSearchParams(searchParams.toString()) if (sorting.field && sorting.direction) { const ordering = sorting.direction === "desc" ? `-${sorting.field}` : sorting.field params.set("ordering", ordering) } else { params.delete("ordering") } router.push(`/transactions?${params.toString()}`) }, [sorting]) const getSortIcon = (field: string) => { if (sorting.field !== field) return null return sorting.direction === "asc" ? ( ) : sorting.direction === "desc" ? ( ) : null } return (
{visibleFields.map((field) => ( field.renderer.sortable && handleSort(field.code)} > {field.name || field.renderer.name} {field.renderer.sortable && getSortIcon(field.code)} ))} {transactions.map((transaction) => ( handleRowClick(transaction.id)} > e.stopPropagation()}> { if (checked !== "indeterminate") { toggleOneRow({ stopPropagation: () => {} } as React.MouseEvent, transaction.id) } }} /> {visibleFields.map((field) => ( {renderFieldInTable(transaction, field)} ))} ))} {visibleFields.map((field) => ( {field.renderer.footerValue ? field.renderer.footerValue(transactions) : ""} ))}
{selectedIds.length > 0 && ( setSelectedIds([])} /> )}
) }