Add clickable rows and standardize action button styles

The changes add row click functionality and consistent action button
styling across data tables. Main updates:

- Add `onRowClick` handler to make rows clickable and navigate to
  details pages
- Add `data-action-button` attribute to exclude action buttons from row
  click
- Fix TypeScript errors and types
This commit is contained in:
2025-07-15 20:07:00 -04:00
parent ea8531bde6
commit 339684d132
15 changed files with 1655 additions and 1961 deletions
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "~/components/ui/button";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
@@ -78,107 +79,125 @@ const formatCurrency = (amount: number) => {
}).format(amount);
};
const columns: ColumnDef<Invoice>[] = [
{
accessorKey: "client.name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Client" />
),
cell: ({ row }) => {
const invoice = row.original;
return (
<div className="min-w-0 max-w-[80px] sm:max-w-[200px] lg:max-w-[300px]">
<p className="truncate font-medium">{invoice.client?.name ?? "—"}</p>
<p className="text-muted-foreground truncate text-xs sm:text-sm">
{invoice.invoiceNumber}
</p>
</div>
);
},
},
{
accessorKey: "issueDate",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Date" />
),
cell: ({ row }) => {
const date = row.getValue("issueDate") as Date;
return (
<div className="min-w-0">
<p className="truncate text-sm">{formatDate(date)}</p>
<p className="text-muted-foreground truncate text-xs">
Due {formatDate(new Date(row.original.dueDate))}
</p>
</div>
);
},
},
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => {
const invoice = row.original;
return <StatusBadge status={getStatusType(invoice)} />;
},
filterFn: (row, id, value) => {
const invoice = row.original;
const status = getStatusType(invoice);
return value.includes(status);
},
meta: {
headerClassName: "hidden sm:table-cell",
cellClassName: "hidden sm:table-cell",
},
},
{
accessorKey: "totalAmount",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Amount" />
),
cell: ({ row }) => {
const amount = row.getValue("totalAmount") as number;
return (
<div className="text-right">
<p className="font-semibold text-sm">{formatCurrency(amount)}</p>
<p className="text-muted-foreground text-xs">
{row.original.items?.length ?? 0} items
</p>
</div>
);
},
meta: {
headerClassName: "hidden sm:table-cell",
cellClassName: "hidden sm:table-cell",
},
},
{
id: "actions",
cell: ({ row }) => {
const invoice = row.original;
return (
<div className="flex items-center justify-end gap-1">
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Eye className="h-3.5 w-3.5" />
</Button>
</Link>
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Edit className="h-3.5 w-3.5" />
</Button>
</Link>
{invoice.items && invoice.client && (
<PDFDownloadButton invoiceId={invoice.id} variant="icon" />
)}
</div>
);
},
},
];
export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
const router = useRouter();
const handleRowClick = (invoice: Invoice) => {
router.push(`/dashboard/invoices/${invoice.id}`);
};
const columns: ColumnDef<Invoice>[] = [
{
accessorKey: "client.name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Client" />
),
cell: ({ row }) => {
const invoice = row.original;
return (
<div className="min-w-0 max-w-[80px] sm:max-w-[200px] lg:max-w-[300px]">
<p className="truncate font-medium">{invoice.client?.name ?? "—"}</p>
<p className="text-muted-foreground truncate text-xs sm:text-sm">
{invoice.invoiceNumber}
</p>
</div>
);
},
},
{
accessorKey: "issueDate",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Date" />
),
cell: ({ row }) => {
const date = row.getValue("issueDate");
return (
<div className="min-w-0">
<p className="truncate text-sm">{formatDate(date as Date)}</p>
<p className="text-muted-foreground truncate text-xs">
Due {formatDate(new Date(row.original.dueDate))}
</p>
</div>
);
},
},
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => {
const invoice = row.original;
return <StatusBadge status={getStatusType(invoice)} />;
},
filterFn: (row, id, value: string[]) => {
const invoice = row.original;
const status = getStatusType(invoice);
return value.includes(status);
},
meta: {
headerClassName: "hidden sm:table-cell",
cellClassName: "hidden sm:table-cell",
},
},
{
accessorKey: "totalAmount",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Amount" />
),
cell: ({ row }) => {
const amount = row.getValue("totalAmount");
return (
<div className="text-right">
<p className="font-semibold text-sm">{formatCurrency(amount as number)}</p>
<p className="text-muted-foreground text-xs">
{row.original.items?.length ?? 0} items
</p>
</div>
);
},
meta: {
headerClassName: "hidden sm:table-cell",
cellClassName: "hidden sm:table-cell",
},
},
{
id: "actions",
cell: ({ row }) => {
const invoice = row.original;
return (
<div className="flex items-center justify-end gap-1">
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
data-action-button="true"
>
<Eye className="h-3.5 w-3.5" />
</Button>
</Link>
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
data-action-button="true"
>
<Edit className="h-3.5 w-3.5" />
</Button>
</Link>
{invoice.items && invoice.client && (
<div data-action-button="true">
<PDFDownloadButton invoiceId={invoice.id} variant="icon" />
</div>
)}
</div>
);
},
},
];
const filterableColumns = [
{
id: "status",
@@ -199,6 +218,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
searchKey="invoiceNumber"
searchPlaceholder="Search invoices..."
filterableColumns={filterableColumns}
onRowClick={handleRowClick}
/>
);
}