ShadCN Vaults

Back to Blocks

Modern Transaction Table

Unknown Block

UnknownComponent

Modern Transaction Table

Modern transaction table with TanStack Table integration, column visibility controls, and advanced filtering.

Preview

Full width desktop view

Code

banking-5.tsx
1"use client";
2
3import * as React from "react";
4import {
5  ColumnDef,
6  ColumnFiltersState,
7  SortingState,
8  VisibilityState,
9  flexRender,
10  getCoreRowModel,
11  getFilteredRowModel,
12  getPaginationRowModel,
13  getSortedRowModel,
14  useReactTable,
15} from "@tanstack/react-table";
16import {
17  ArrowUpDown,
18  ChevronDown,
19  MoreHorizontal,
20  CreditCard,
21  Building,
22  Clock,
23  CheckCircle,
24  XCircle,
25  AlertCircle,
26} from "lucide-react";
27import { format, getDate } from "date-fns";
28
29import { Button } from "@/components/ui/button";
30import { Checkbox } from "@/components/ui/checkbox";
31import {
32  DropdownMenu,
33  DropdownMenuCheckboxItem,
34  DropdownMenuContent,
35  DropdownMenuItem,
36  DropdownMenuLabel,
37  DropdownMenuSeparator,
38  DropdownMenuTrigger,
39} from "@/components/ui/dropdown-menu";
40import { Input } from "@/components/ui/input";
41import {
42  Table,
43  TableBody,
44  TableCell,
45  TableHead,
46  TableHeader,
47  TableRow,
48} from "@/components/ui/table";
49import { Badge } from "@/components/ui/badge";
50import {
51  Card,
52  CardContent,
53  CardDescription,
54  CardHeader,
55  CardTitle,
56} from "@/components/ui/card";
57import {
58  Tooltip,
59  TooltipContent,
60  TooltipProvider,
61  TooltipTrigger,
62} from "@/components/ui/tooltip";
63
64const cn = (...classes: string[]) => classes.filter(Boolean).join(" ");
65
66function DateBadge({
67  date: rawDate,
68  time = false,
69  tooltip = true,
70}: {
71  date: string | Date;
72  time?: boolean;
73  tooltip?: boolean;
74}) {
75  const date = getDate(rawDate);
76  const month = format(rawDate, "LLL");
77  const fullDate = format(rawDate, time ? "PPPP - kk:mm" : "PPPP");
78
79  return (
80    <TooltipProvider>
81      <Tooltip>
82        <TooltipTrigger asChild>
83          <div className="bg-background/40 flex size-10 shrink-0 cursor-default flex-col items-center justify-center rounded-md border text-center">
84            <span className="text-sm font-semibold leading-snug">{date}</span>
85            <span className="text-muted-foreground text-xs leading-none">
86              {month}
87            </span>
88          </div>
89        </TooltipTrigger>
90        <TooltipContent className="shadow-md">{fullDate}</TooltipContent>
91      </Tooltip>
92    </TooltipProvider>
93  );
94}
95
96interface MoneyTransferTransaction {
97  id: string;
98  transactionId: string;
99  amount: number;
100  currency: string;
101  destination: {
102    name: string;
103    accountNumber: string;
104    bankName: string;
105  };
106  paymentDateTime: Date;
107  disbursementDateTime: Date | null;
108  paymentStatus: "pending" | "processing" | "completed" | "failed";
109  disbursementStatus: "pending" | "processing" | "completed" | "failed";
110  paymentProcessor: string;
111  platformFee: number;
112  paymentFee: number;
113  disbursementFee: number;
114  totalFees: number;
115  netAmount: number;
116}
117
118const mockData: MoneyTransferTransaction[] = [
119  {
120    id: "1",
121    transactionId: "TXN-2024-001",
122    amount: 1000.0,
123    currency: "USD",
124    destination: {
125      name: "John Doe",
126      accountNumber: "****1234",
127      bankName: "Chase Bank",
128    },
129    paymentDateTime: new Date("2024-01-15T10:30:00"),
130    disbursementDateTime: new Date("2024-01-15T14:45:00"),
131    paymentStatus: "completed",
132    disbursementStatus: "completed",
133    paymentProcessor: "Stripe",
134    platformFee: 15.0,
135    paymentFee: 2.9,
136    disbursementFee: 1.5,
137    totalFees: 19.4,
138    netAmount: 980.6,
139  },
140  {
141    id: "2",
142    transactionId: "TXN-2024-002",
143    amount: 750.0,
144    currency: "USD",
145    destination: {
146      name: "Jane Smith",
147      accountNumber: "****5678",
148      bankName: "Bank of America",
149    },
150    paymentDateTime: new Date("2024-01-16T09:15:00"),
151    disbursementDateTime: null,
152    paymentStatus: "completed",
153    disbursementStatus: "processing",
154    paymentProcessor: "PayPal",
155    platformFee: 11.25,
156    paymentFee: 2.2,
157    disbursementFee: 1.25,
158    totalFees: 14.7,
159    netAmount: 735.3,
160  },
161  {
162    id: "3",
163    transactionId: "TXN-2024-003",
164    amount: 2500.0,
165    currency: "USD",
166    destination: {
167      name: "Robert Johnson",
168      accountNumber: "****9012",
169      bankName: "Wells Fargo",
170    },
171    paymentDateTime: new Date("2024-01-17T16:20:00"),
172    disbursementDateTime: new Date("2024-01-18T11:30:00"),
173    paymentStatus: "completed",
174    disbursementStatus: "completed",
175    paymentProcessor: "Square",
176    platformFee: 37.5,
177    paymentFee: 7.25,
178    disbursementFee: 3.75,
179    totalFees: 48.5,
180    netAmount: 2451.5,
181  },
182  {
183    id: "4",
184    transactionId: "TXN-2024-004",
185    amount: 500.0,
186    currency: "USD",
187    destination: {
188      name: "Maria Garcia",
189      accountNumber: "****3456",
190      bankName: "Citibank",
191    },
192    paymentDateTime: new Date("2024-01-18T13:45:00"),
193    disbursementDateTime: null,
194    paymentStatus: "failed",
195    disbursementStatus: "pending",
196    paymentProcessor: "Stripe",
197    platformFee: 7.5,
198    paymentFee: 1.45,
199    disbursementFee: 0.75,
200    totalFees: 9.7,
201    netAmount: 490.3,
202  },
203  {
204    id: "5",
205    transactionId: "TXN-2024-005",
206    amount: 1250.0,
207    currency: "USD",
208    destination: {
209      name: "David Wilson",
210      accountNumber: "****7890",
211      bankName: "TD Bank",
212    },
213    paymentDateTime: new Date("2024-01-19T08:30:00"),
214    disbursementDateTime: null,
215    paymentStatus: "processing",
216    disbursementStatus: "pending",
217    paymentProcessor: "PayPal",
218    platformFee: 18.75,
219    paymentFee: 3.63,
220    disbursementFee: 1.88,
221    totalFees: 24.26,
222    netAmount: 1225.74,
223  },
224];
225
226function StatusBadge({
227  status,
228  type,
229}: {
230  status: string;
231  type: "payment" | "disbursement";
232}) {
233  const getStatusConfig = (status: string) => {
234    switch (status) {
235      case "completed":
236        return {
237          icon: CheckCircle,
238          variant: "default",
239          className: "bg-green-100 text-green-800 hover:bg-green-100",
240        };
241      case "processing":
242        return {
243          icon: Clock,
244          variant: "secondary",
245          className: "bg-yellow-100 text-yellow-800 hover:bg-yellow-100",
246        };
247      case "pending":
248        return {
249          icon: AlertCircle,
250          variant: "outline",
251          className: "bg-gray-100 text-gray-800 hover:bg-gray-100",
252        };
253      case "failed":
254        return {
255          icon: XCircle,
256          variant: "destructive",
257          className: "bg-red-100 text-red-800 hover:bg-red-100",
258        };
259      default:
260        return { icon: AlertCircle, variant: "outline", className: "" };
261    }
262  };
263
264  const config = getStatusConfig(status);
265  const Icon = config.icon;
266
267  return (
268    <Badge
269      variant={config.variant as any}
270      className={cn("flex items-center gap-1", config.className)}
271    >
272      <Icon className="h-3 w-3" />
273      <span className="capitalize">{status}</span>
274    </Badge>
275  );
276}
277
278const columns: ColumnDef<MoneyTransferTransaction>[] = [
279  {
280    id: "select",
281    header: ({ table }) => (
282      <Checkbox
283        checked={
284          table.getIsAllPageRowsSelected() ||
285          (table.getIsSomePageRowsSelected() && "indeterminate")
286        }
287        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
288        aria-label="Select all"
289      />
290    ),
291    cell: ({ row }) => (
292      <Checkbox
293        checked={row.getIsSelected()}
294        onCheckedChange={(value) => row.toggleSelected(!!value)}
295        aria-label="Select row"
296      />
297    ),
298    enableSorting: false,
299    enableHiding: false,
300  },
301  {
302    accessorKey: "transactionId",
303    header: ({ column }) => {
304      return (
305        <Button
306          variant="ghost"
307          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
308          className="h-8 px-2"
309        >
310          Transaction ID
311          <ArrowUpDown className="ml-2 h-4 w-4" />
312        </Button>
313      );
314    },
315    cell: ({ row }) => (
316      <div className="font-mono text-sm">{row.getValue("transactionId")}</div>
317    ),
318  },
319  {
320    accessorKey: "paymentDateTime",
321    header: "Payment Date",
322    cell: ({ row }) => (
323      <DateBadge date={row.getValue("paymentDateTime")} time />
324    ),
325  },
326  {
327    accessorKey: "disbursementDateTime",
328    header: "Disbursement Date",
329    cell: ({ row }) => {
330      const date = row.getValue("disbursementDateTime") as Date | null;
331      return date ? (
332        <DateBadge date={date} time />
333      ) : (
334        <span className="text-muted-foreground text-sm">Pending</span>
335      );
336    },
337  },
338  {
339    accessorKey: "amount",
340    header: ({ column }) => {
341      return (
342        <Button
343          variant="ghost"
344          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
345          className="h-8 px-2"
346        >
347          Amount
348          <ArrowUpDown className="ml-2 h-4 w-4" />
349        </Button>
350      );
351    },
352    cell: ({ row }) => {
353      const amount = parseFloat(row.getValue("amount"));
354      const currency = row.original.currency;
355      const formatted = new Intl.NumberFormat("en-US", {
356        style: "currency",
357        currency: currency,
358      }).format(amount);
359
360      return <div className="font-medium">{formatted}</div>;
361    },
362  },
363  {
364    accessorKey: "destination",
365    header: "Destination",
366    cell: ({ row }) => {
367      const destination = row.getValue(
368        "destination"
369      ) as MoneyTransferTransaction["destination"];
370      return (
371        <div className="space-y-1">
372          <div className="font-medium">{destination.name}</div>
373          <div className="text-sm text-muted-foreground flex items-center gap-1">
374            <Building className="h-3 w-3" />
375            {destination.bankName}
376          </div>
377          <div className="text-xs text-muted-foreground font-mono">
378            {destination.accountNumber}
379          </div>
380        </div>
381      );
382    },
383  },
384  {
385    accessorKey: "paymentStatus",
386    header: "Payment Status",
387    cell: ({ row }) => (
388      <StatusBadge status={row.getValue("paymentStatus")} type="payment" />
389    ),
390  },
391  {
392    accessorKey: "disbursementStatus",
393    header: "Disbursement Status",
394    cell: ({ row }) => (
395      <StatusBadge
396        status={row.getValue("disbursementStatus")}
397        type="disbursement"
398      />
399    ),
400  },
401  {
402    accessorKey: "paymentProcessor",
403    header: "Payment Processor",
404    cell: ({ row }) => (
405      <div className="flex items-center gap-2">
406        <CreditCard className="h-4 w-4 text-muted-foreground" />
407        <span>{row.getValue("paymentProcessor")}</span>
408      </div>
409    ),
410  },
411  {
412    accessorKey: "fees",
413    header: "Fees Breakdown",
414    cell: ({ row }) => {
415      const transaction = row.original;
416      return (
417        <TooltipProvider>
418          <Tooltip>
419            <TooltipTrigger asChild>
420              <div className="space-y-1 cursor-help">
421                <div className="text-sm font-medium">
422                  ${transaction.totalFees.toFixed(2)}
423                </div>
424                <div className="text-xs text-muted-foreground">
425                  Platform: ${transaction.platformFee.toFixed(2)}
426                </div>
427              </div>
428            </TooltipTrigger>
429            <TooltipContent className="space-y-1">
430              <div>Platform Fee: ${transaction.platformFee.toFixed(2)}</div>
431              <div>Payment Fee: ${transaction.paymentFee.toFixed(2)}</div>
432              <div>
433                Disbursement Fee: ${transaction.disbursementFee.toFixed(2)}
434              </div>
435              <div className="border-t pt-1 font-medium">
436                Total: ${transaction.totalFees.toFixed(2)}
437              </div>
438            </TooltipContent>
439          </Tooltip>
440        </TooltipProvider>
441      );
442    },
443  },
444  {
445    accessorKey: "netAmount",
446    header: ({ column }) => {
447      return (
448        <Button
449          variant="ghost"
450          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
451          className="h-8 px-2"
452        >
453          Net Amount
454          <ArrowUpDown className="ml-2 h-4 w-4" />
455        </Button>
456      );
457    },
458    cell: ({ row }) => {
459      const netAmount = parseFloat(row.getValue("netAmount"));
460      const currency = row.original.currency;
461      const formatted = new Intl.NumberFormat("en-US", {
462        style: "currency",
463        currency: currency,
464      }).format(netAmount);
465
466      return <div className="font-medium text-green-600">{formatted}</div>;
467    },
468  },
469  {
470    id: "actions",
471    enableHiding: false,
472    cell: ({ row }) => {
473      const transaction = row.original;
474
475      return (
476        <DropdownMenu>
477          <DropdownMenuTrigger asChild>
478            <Button variant="ghost" className="h-8 w-8 p-0">
479              <span className="sr-only">Open menu</span>
480              <MoreHorizontal className="h-4 w-4" />
481            </Button>
482          </DropdownMenuTrigger>
483          <DropdownMenuContent align="end">
484            <DropdownMenuLabel>Actions</DropdownMenuLabel>
485            <DropdownMenuItem
486              onClick={() =>
487                navigator.clipboard.writeText(transaction.transactionId)
488              }
489            >
490              Copy transaction ID
491            </DropdownMenuItem>
492            <DropdownMenuSeparator />
493            <DropdownMenuItem>View details</DropdownMenuItem>
494            <DropdownMenuItem>Download receipt</DropdownMenuItem>
495            <DropdownMenuItem>Refund transaction</DropdownMenuItem>
496          </DropdownMenuContent>
497        </DropdownMenu>
498      );
499    },
500  },
501];
502
503function MoneyTransferTransactionTable() {
504  const [sorting, setSorting] = React.useState<SortingState>([]);
505  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
506    []
507  );
508  const [columnVisibility, setColumnVisibility] =
509    React.useState<VisibilityState>({});
510  const [rowSelection, setRowSelection] = React.useState({});
511
512  const table = useReactTable({
513    data: mockData,
514    columns,
515    onSortingChange: setSorting,
516    onColumnFiltersChange: setColumnFilters,
517    getCoreRowModel: getCoreRowModel(),
518    getPaginationRowModel: getPaginationRowModel(),
519    getSortedRowModel: getSortedRowModel(),
520    getFilteredRowModel: getFilteredRowModel(),
521    onColumnVisibilityChange: setColumnVisibility,
522    onRowSelectionChange: setRowSelection,
523    state: {
524      sorting,
525      columnFilters,
526      columnVisibility,
527      rowSelection,
528    },
529  });
530
531  const totalAmount = mockData.reduce(
532    (sum, transaction) => sum + transaction.amount,
533    0
534  );
535  const totalFees = mockData.reduce(
536    (sum, transaction) => sum + transaction.totalFees,
537    0
538  );
539  const totalPlatformFees = mockData.reduce(
540    (sum, transaction) => sum + transaction.platformFee,
541    0
542  );
543
544  return (
545    <div className="w-full space-y-6">
546      <Card>
547        <CardHeader>
548          <CardTitle>Money Transfer Transactions</CardTitle>
549          <CardDescription>
550            Detailed view of all money transfer transactions with payment and
551            disbursement tracking
552          </CardDescription>
553        </CardHeader>
554        <CardContent>
555          <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
556            <div className="bg-muted/50 p-4 rounded-lg">
557              <div className="text-sm text-muted-foreground">Total Volume</div>
558              <div className="text-2xl font-bold">
559                $
560                {totalAmount.toLocaleString("en-US", {
561                  minimumFractionDigits: 2,
562                })}
563              </div>
564            </div>
565            <div className="bg-muted/50 p-4 rounded-lg">
566              <div className="text-sm text-muted-foreground">
567                Platform Revenue
568              </div>
569              <div className="text-2xl font-bold text-green-600">
570                $
571                {totalPlatformFees.toLocaleString("en-US", {
572                  minimumFractionDigits: 2,
573                })}
574              </div>
575            </div>
576            <div className="bg-muted/50 p-4 rounded-lg">
577              <div className="text-sm text-muted-foreground">Total Fees</div>
578              <div className="text-2xl font-bold">
579                $
580                {totalFees.toLocaleString("en-US", {
581                  minimumFractionDigits: 2,
582                })}
583              </div>
584            </div>
585          </div>
586
587          <div className="flex items-center py-4 gap-4">
588            <Input
589              placeholder="Filter by transaction ID..."
590              value={
591                (table
592                  .getColumn("transactionId")
593                  ?.getFilterValue() as string) ?? ""
594              }
595              onChange={(event) =>
596                table
597                  .getColumn("transactionId")
598                  ?.setFilterValue(event.target.value)
599              }
600              className="max-w-sm"
601            />
602            <DropdownMenu>
603              <DropdownMenuTrigger asChild>
604                <Button variant="outline" className="ml-auto">
605                  Columns <ChevronDown className="ml-2 h-4 w-4" />
606                </Button>
607              </DropdownMenuTrigger>
608              <DropdownMenuContent align="end">
609                {table
610                  .getAllColumns()
611                  .filter((column) => column.getCanHide())
612                  .map((column) => {
613                    return (
614                      <DropdownMenuCheckboxItem
615                        key={column.id}
616                        className="capitalize"
617                        checked={column.getIsVisible()}
618                        onCheckedChange={(value) =>
619                          column.toggleVisibility(!!value)
620                        }
621                      >
622                        {column.id}
623                      </DropdownMenuCheckboxItem>
624                    );
625                  })}
626              </DropdownMenuContent>
627            </DropdownMenu>
628          </div>
629
630          <div className="rounded-md border">
631            <Table>
632              <TableHeader>
633                {table.getHeaderGroups().map((headerGroup) => (
634                  <TableRow key={headerGroup.id}>
635                    {headerGroup.headers.map((header) => {
636                      return (
637                        <TableHead key={header.id}>
638                          {header.isPlaceholder
639                            ? null
640                            : flexRender(
641                                header.column.columnDef.header,
642                                header.getContext()
643                              )}
644                        </TableHead>
645                      );
646                    })}
647                  </TableRow>
648                ))}
649              </TableHeader>
650              <TableBody>
651                {table.getRowModel().rows?.length ? (
652                  table.getRowModel().rows.map((row) => (
653                    <TableRow
654                      key={row.id}
655                      data-state={row.getIsSelected() && "selected"}
656                    >
657                      {row.getVisibleCells().map((cell) => (
658                        <TableCell key={cell.id}>
659                          {flexRender(
660                            cell.column.columnDef.cell,
661                            cell.getContext()
662                          )}
663                        </TableCell>
664                      ))}
665                    </TableRow>
666                  ))
667                ) : (
668                  <TableRow>
669                    <TableCell
670                      colSpan={columns.length}
671                      className="h-24 text-center"
672                    >
673                      No results.
674                    </TableCell>
675                  </TableRow>
676                )}
677              </TableBody>
678            </Table>
679          </div>
680
681          <div className="flex items-center justify-end space-x-2 py-4">
682            <div className="flex-1 text-sm text-muted-foreground">
683              {table.getFilteredSelectedRowModel().rows.length} of{" "}
684              {table.getFilteredRowModel().rows.length} row(s) selected.
685            </div>
686            <div className="space-x-2">
687              <Button
688                variant="outline"
689                size="sm"
690                onClick={() => table.previousPage()}
691                disabled={!table.getCanPreviousPage()}
692              >
693                Previous
694              </Button>
695              <Button
696                variant="outline"
697                size="sm"
698                onClick={() => table.nextPage()}
699                disabled={!table.getCanNextPage()}
700              >
701                Next
702              </Button>
703            </div>
704          </div>
705        </CardContent>
706      </Card>
707    </div>
708  );
709}
710
711export default MoneyTransferTransactionTable;

Dependencies

External Libraries

@tanstack/react-tabledate-fnslucide-reactreact

Shadcn/UI Components

badgebuttoncardcheckboxdropdown-menuinputtabletooltip

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.