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