ShadcnUI Vaults

Back to Blocks

Advanced Transaction Table

Unknown Block

UnknownComponent

Advanced Transaction Table

Feature-rich transaction table with advanced filtering, sorting, and detailed money transfer analytics.

Preview

Full width desktop view

Code

banking-2.tsx
1"use client";
2
3import React, { useState, useMemo } from "react";
4import {
5  ChevronUp,
6  ChevronDown,
7  Search,
8  Filter,
9  Calendar,
10  DollarSign,
11  CreditCard,
12  Building,
13  Clock,
14  CheckCircle,
15  XCircle,
16  AlertCircle,
17  Eye,
18} from "lucide-react";
19import { cva, type VariantProps } from "class-variance-authority";
20
21// Utility function
22function cn(...classes: (string | undefined | null | boolean)[]): string {
23  return classes.filter(Boolean).join(" ");
24}
25
26// Badge component
27const badgeVariants = cva(
28  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
29  {
30    variants: {
31      variant: {
32        default:
33          "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
34        secondary:
35          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
36        destructive:
37          "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
38        outline: "text-foreground",
39        success:
40          "border-transparent bg-green-500 text-white hover:bg-green-600",
41        warning:
42          "border-transparent bg-yellow-500 text-white hover:bg-yellow-600",
43        pending: "border-transparent bg-blue-500 text-white hover:bg-blue-600",
44      },
45    },
46    defaultVariants: {
47      variant: "default",
48    },
49  }
50);
51
52interface BadgeProps
53  extends React.HTMLAttributes<HTMLDivElement>,
54    VariantProps<typeof badgeVariants> {}
55
56function Badge({ className, variant, ...props }: BadgeProps) {
57  return (
58    <div className={cn(badgeVariants({ variant }), className)} {...props} />
59  );
60}
61
62// Status Badge component
63const statusBadgeVariants = cva(
64  "inline-flex items-center gap-x-2.5 rounded-full bg-background px-2.5 py-1.5 text-xs border",
65  {
66    variants: {
67      status: {
68        success: "",
69        error: "",
70        default: "",
71      },
72    },
73    defaultVariants: {
74      status: "default",
75    },
76  }
77);
78
79interface StatusBadgeProps
80  extends React.HTMLAttributes<HTMLSpanElement>,
81    VariantProps<typeof statusBadgeVariants> {
82  leftIcon?: React.ComponentType<{ className?: string }>;
83  rightIcon?: React.ComponentType<{ className?: string }>;
84  leftLabel: string;
85  rightLabel: string;
86}
87
88function StatusBadge({
89  className,
90  status,
91  leftIcon: LeftIcon,
92  rightIcon: RightIcon,
93  leftLabel,
94  rightLabel,
95  ...props
96}: StatusBadgeProps) {
97  return (
98    <span className={cn(statusBadgeVariants({ status }), className)} {...props}>
99      <span className="inline-flex items-center gap-1.5 font-medium text-foreground">
100        {LeftIcon && (
101          <LeftIcon
102            className={cn(
103              "-ml-0.5 size-4 shrink-0",
104              status === "success" && "text-emerald-600 dark:text-emerald-500",
105              status === "error" && "text-red-600 dark:text-red-500"
106            )}
107          />
108        )}
109        {leftLabel}
110      </span>
111      <span className="h-4 w-px bg-border" />
112      <span className="inline-flex items-center gap-1.5 text-muted-foreground">
113        {RightIcon && <RightIcon className="-ml-0.5 size-4 shrink-0" />}
114        {rightLabel}
115      </span>
116    </span>
117  );
118}
119
120// Data Table Types
121export type DataTableColumn<T> = {
122  key: keyof T;
123  header: string;
124  sortable?: boolean;
125  filterable?: boolean;
126  render?: (value: any, row: T) => React.ReactNode;
127  width?: string;
128};
129
130export type DataTableProps<T> = {
131  data: T[];
132  columns: DataTableColumn<T>[];
133  className?: string;
134  searchable?: boolean;
135  searchPlaceholder?: string;
136  itemsPerPage?: number;
137  showPagination?: boolean;
138  striped?: boolean;
139  hoverable?: boolean;
140  bordered?: boolean;
141  compact?: boolean;
142  loading?: boolean;
143  emptyMessage?: string;
144  onRowClick?: (row: T, index: number) => void;
145};
146
147// Data Table Component
148function DataTable<T extends Record<string, any>>({
149  data,
150  columns,
151  className,
152  searchable = true,
153  searchPlaceholder = "Search transactions...",
154  itemsPerPage = 10,
155  showPagination = true,
156  striped = false,
157  hoverable = true,
158  bordered = true,
159  compact = false,
160  loading = false,
161  emptyMessage = "No transactions found",
162  onRowClick,
163}: DataTableProps<T>) {
164  const [search, setSearch] = useState("");
165  const [sortConfig, setSortConfig] = useState<{
166    key: keyof T | null;
167    direction: "asc" | "desc";
168  }>({ key: null, direction: "asc" });
169  const [currentPage, setCurrentPage] = useState(1);
170  const [columnFilters, setColumnFilters] = useState<Record<string, string>>(
171    {}
172  );
173
174  // Filter data based on search and column filters
175  const filteredData = useMemo(() => {
176    let filtered = [...data];
177
178    // Global search
179    if (search) {
180      filtered = filtered.filter((row) =>
181        columns.some((column) => {
182          const value = row[column.key];
183          return value?.toString().toLowerCase().includes(search.toLowerCase());
184        })
185      );
186    }
187
188    // Column filters
189    Object.entries(columnFilters).forEach(([key, value]) => {
190      if (value) {
191        filtered = filtered.filter((row) => {
192          const rowValue = row[key as keyof T];
193          return rowValue
194            ?.toString()
195            .toLowerCase()
196            .includes(value.toLowerCase());
197        });
198      }
199    });
200
201    return filtered;
202  }, [data, search, columnFilters, columns]);
203
204  // Sort data
205  const sortedData = useMemo(() => {
206    if (!sortConfig.key) return filteredData;
207
208    return [...filteredData].sort((a, b) => {
209      const aValue = a[sortConfig.key!];
210      const bValue = b[sortConfig.key!];
211
212      if (aValue < bValue) {
213        return sortConfig.direction === "asc" ? -1 : 1;
214      }
215      if (aValue > bValue) {
216        return sortConfig.direction === "asc" ? 1 : -1;
217      }
218      return 0;
219    });
220  }, [filteredData, sortConfig]);
221
222  // Pagination
223  const paginatedData = useMemo(() => {
224    if (!showPagination) return sortedData;
225
226    const startIndex = (currentPage - 1) * itemsPerPage;
227    return sortedData.slice(startIndex, startIndex + itemsPerPage);
228  }, [sortedData, currentPage, itemsPerPage, showPagination]);
229
230  const totalPages = Math.ceil(sortedData.length / itemsPerPage);
231
232  const handleSort = (key: keyof T) => {
233    setSortConfig((current) => ({
234      key,
235      direction:
236        current.key === key && current.direction === "asc" ? "desc" : "asc",
237    }));
238  };
239
240  const handleColumnFilter = (key: string, value: string) => {
241    setColumnFilters((prev) => ({
242      ...prev,
243      [key]: value,
244    }));
245    setCurrentPage(1);
246  };
247
248  const clearColumnFilter = (key: string) => {
249    setColumnFilters((prev) => {
250      const newFilters = { ...prev };
251      delete newFilters[key];
252      return newFilters;
253    });
254  };
255
256  if (loading) {
257    return (
258      <div className={cn("w-full bg-background rounded-lg border", className)}>
259        <div className="animate-pulse p-6">
260          {searchable && <div className="mb-6 h-11 bg-muted rounded-lg"></div>}
261          <div className="border border-border overflow-hidden rounded-lg">
262            <div className="bg-muted/30 h-14"></div>
263            {Array.from({ length: 5 }).map((_, i) => (
264              <div
265                key={i}
266                className="h-14 border-t border-border bg-background"
267              ></div>
268            ))}
269          </div>
270          <div className="mt-6 flex justify-between items-center">
271            <div className="h-4 bg-muted rounded w-48"></div>
272            <div className="flex gap-2">
273              <div className="h-9 w-20 bg-muted rounded-lg"></div>
274              <div className="h-9 w-9 bg-muted rounded-lg"></div>
275              <div className="h-9 w-9 bg-muted rounded-lg"></div>
276              <div className="h-9 w-16 bg-muted rounded-lg"></div>
277            </div>
278          </div>
279        </div>
280      </div>
281    );
282  }
283
284  return (
285    <div
286      className={cn(
287        "w-full bg-background rounded-lg",
288        bordered && "border border-border",
289        className
290      )}
291    >
292      {/* Search and Filters */}
293      {searchable && (
294        <div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 p-6 border-b border-border">
295          <div className="relative w-full sm:w-auto sm:flex-1 sm:max-w-sm">
296            <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
297            <input
298              type="text"
299              placeholder={searchPlaceholder}
300              value={search}
301              onChange={(e) => {
302                setSearch(e.target.value);
303                setCurrentPage(1);
304              }}
305              className="w-full pl-10 pr-4 py-2.5 border border-input rounded-lg bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-all"
306            />
307          </div>
308        </div>
309      )}
310
311      {/* Table */}
312      <div
313        className={cn(
314          "overflow-hidden",
315          searchable ? "rounded-b-lg" : "rounded-lg"
316        )}
317      >
318        <div className="overflow-x-auto">
319          <table className="w-full min-w-full">
320            <thead className="bg-muted/30">
321              <tr>
322                {columns.map((column) => (
323                  <th
324                    key={String(column.key)}
325                    className={cn(
326                      "text-left font-medium text-muted-foreground bg-muted/30",
327                      compact ? "px-4 py-3" : "px-6 py-4",
328                      column.sortable &&
329                        "cursor-pointer hover:bg-muted/50 transition-colors",
330                      column.width && `w-[${column.width}]`
331                    )}
332                    onClick={() => column.sortable && handleSort(column.key)}
333                    style={column.width ? { width: column.width } : undefined}
334                  >
335                    <div className="flex items-center justify-between">
336                      <div className="flex items-center gap-2">
337                        <span className="text-sm font-semibold">
338                          {column.header}
339                        </span>
340                        {column.sortable && (
341                          <div className="flex flex-col">
342                            <ChevronUp
343                              className={cn(
344                                "h-3 w-3",
345                                sortConfig.key === column.key &&
346                                  sortConfig.direction === "asc"
347                                  ? "text-primary"
348                                  : "text-muted-foreground/40"
349                              )}
350                            />
351                            <ChevronDown
352                              className={cn(
353                                "h-3 w-3 -mt-1",
354                                sortConfig.key === column.key &&
355                                  sortConfig.direction === "desc"
356                                  ? "text-primary"
357                                  : "text-muted-foreground/40"
358                              )}
359                            />
360                          </div>
361                        )}
362                      </div>
363                      {column.filterable && (
364                        <div className="relative">
365                          <Filter className="h-3 w-3 text-muted-foreground/50" />
366                        </div>
367                      )}
368                    </div>
369                    {/* Column Filter */}
370                    {column.filterable && (
371                      <div className="mt-3">
372                        <input
373                          type="text"
374                          placeholder="Filter..."
375                          value={columnFilters[String(column.key)] || ""}
376                          onChange={(e) =>
377                            handleColumnFilter(
378                              String(column.key),
379                              e.target.value
380                            )
381                          }
382                          onClick={(e) => e.stopPropagation()}
383                          className="w-full px-3 py-1.5 text-xs border border-input rounded-md bg-background focus:outline-none focus:ring-1 focus:ring-ring transition-all"
384                        />
385                        {columnFilters[String(column.key)] && (
386                          <button
387                            onClick={(e) => {
388                              e.stopPropagation();
389                              clearColumnFilter(String(column.key));
390                            }}
391                            className="absolute right-2 top-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
392                          >
393394                          </button>
395                        )}
396                      </div>
397                    )}
398                  </th>
399                ))}
400              </tr>
401            </thead>
402            <tbody className="bg-background">
403              {paginatedData.length === 0 ? (
404                <tr>
405                  <td
406                    colSpan={columns.length}
407                    className={cn(
408                      "text-center text-muted-foreground bg-background",
409                      compact ? "px-4 py-12" : "px-6 py-16"
410                    )}
411                  >
412                    <div className="flex flex-col items-center space-y-2">
413                      <div className="text-4xl">💸</div>
414                      <div className="font-medium">{emptyMessage}</div>
415                    </div>
416                  </td>
417                </tr>
418              ) : (
419                paginatedData.map((row, index) => (
420                  <tr
421                    key={index}
422                    className={cn(
423                      "border-t border-border bg-background transition-colors",
424                      striped && index % 2 === 0 && "bg-muted/20",
425                      hoverable && "hover:bg-muted/30",
426                      onRowClick && "cursor-pointer"
427                    )}
428                    onClick={() => onRowClick?.(row, index)}
429                  >
430                    {columns.map((column) => (
431                      <td
432                        key={String(column.key)}
433                        className={cn(
434                          "text-sm text-foreground",
435                          compact ? "px-4 py-3" : "px-6 py-4"
436                        )}
437                      >
438                        {column.render
439                          ? column.render(row[column.key], row)
440                          : String(row[column.key] ?? "")}
441                      </td>
442                    ))}
443                  </tr>
444                ))
445              )}
446            </tbody>
447          </table>
448        </div>
449      </div>
450
451      {/* Pagination */}
452      {showPagination && totalPages > 1 && (
453        <div className="flex flex-col sm:flex-row items-center justify-between gap-4 p-6 bg-background border-t border-border">
454          <div className="text-sm text-muted-foreground order-2 sm:order-1">
455            Showing {(currentPage - 1) * itemsPerPage + 1} to{" "}
456            {Math.min(currentPage * itemsPerPage, sortedData.length)} of{" "}
457            {sortedData.length} results
458          </div>
459          <div className="flex items-center gap-2 order-1 sm:order-2">
460            <button
461              onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
462              disabled={currentPage === 1}
463              className="px-3 py-2 text-sm border border-input rounded-lg hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
464            >
465              Previous
466            </button>
467            <div className="hidden sm:flex items-center gap-1">
468              {Array.from({ length: Math.min(totalPages, 5) }).map((_, i) => {
469                const pageNumber =
470                  currentPage <= 3
471                    ? i + 1
472                    : currentPage >= totalPages - 2
473                    ? totalPages - 4 + i
474                    : currentPage - 2 + i;
475
476                if (pageNumber < 1 || pageNumber > totalPages) return null;
477
478                return (
479                  <button
480                    key={pageNumber}
481                    onClick={() => setCurrentPage(pageNumber)}
482                    className={cn(
483                      "px-3 py-2 text-sm border border-input rounded-lg hover:bg-muted transition-colors",
484                      currentPage === pageNumber &&
485                        "bg-primary text-primary-foreground border-primary hover:bg-primary/90"
486                    )}
487                  >
488                    {pageNumber}
489                  </button>
490                );
491              })}
492            </div>
493            <button
494              onClick={() =>
495                setCurrentPage((prev) => Math.min(prev + 1, totalPages))
496              }
497              disabled={currentPage === totalPages}
498              className="px-3 py-2 text-sm border border-input rounded-lg hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
499            >
500              Next
501            </button>
502          </div>
503        </div>
504      )}
505    </div>
506  );
507}
508
509// Transaction interface
510interface MoneyTransferTransaction {
511  id: string;
512  transactionId: string;
513  amount: number;
514  currency: string;
515  destination: string;
516  destinationType: "bank" | "wallet" | "card";
517  paymentDate: string;
518  disbursementDate: string;
519  paymentStatus: "completed" | "pending" | "failed";
520  disbursementStatus: "completed" | "pending" | "failed";
521  paymentProcessor: string;
522  platformFee: number;
523  paymentFee: number;
524  disbursementFee: number;
525  netAmount: number;
526  description?: string;
527}
528
529// Status rendering function
530const renderStatus = (status: string) => {
531  const statusConfig = {
532    completed: {
533      variant: "success" as const,
534      icon: CheckCircle,
535      label: "Completed",
536    },
537    pending: { variant: "pending" as const, icon: Clock, label: "Pending" },
538    failed: { variant: "destructive" as const, icon: XCircle, label: "Failed" },
539  };
540
541  const config =
542    statusConfig[status as keyof typeof statusConfig] || statusConfig.pending;
543  const Icon = config.icon;
544
545  return (
546    <Badge variant={config.variant} className="gap-1">
547      <Icon className="h-3 w-3" />
548      {config.label}
549    </Badge>
550  );
551};
552
553// Currency formatter
554const formatCurrency = (amount: number, currency: string = "USD") => {
555  return new Intl.NumberFormat("en-US", {
556    style: "currency",
557    currency: currency,
558  }).format(amount);
559};
560
561// Date formatter
562const formatDate = (dateString: string) => {
563  return new Date(dateString).toLocaleString("en-US", {
564    year: "numeric",
565    month: "short",
566    day: "numeric",
567    hour: "2-digit",
568    minute: "2-digit",
569  });
570};
571
572// Main component
573function MoneyTransferTransactionList() {
574  const [selectedTransaction, setSelectedTransaction] =
575    useState<MoneyTransferTransaction | null>(null);
576
577  // Sample data
578  const transactions: MoneyTransferTransaction[] = [
579    {
580      id: "1",
581      transactionId: "TXN-2024-001",
582      amount: 1500.0,
583      currency: "USD",
584      destination: "**** **** **** 1234",
585      destinationType: "card",
586      paymentDate: "2024-01-15T10:30:00Z",
587      disbursementDate: "2024-01-15T11:45:00Z",
588      paymentStatus: "completed",
589      disbursementStatus: "completed",
590      paymentProcessor: "Stripe",
591      platformFee: 15.0,
592      paymentFee: 2.5,
593      disbursementFee: 1.0,
594      netAmount: 1481.5,
595      description: "Salary payment",
596    },
597    {
598      id: "2",
599      transactionId: "TXN-2024-002",
600      amount: 750.0,
601      currency: "USD",
602      destination: "wallet@example.com",
603      destinationType: "wallet",
604      paymentDate: "2024-01-14T14:20:00Z",
605      disbursementDate: "2024-01-14T14:25:00Z",
606      paymentStatus: "completed",
607      disbursementStatus: "pending",
608      paymentProcessor: "PayPal",
609      platformFee: 7.5,
610      paymentFee: 1.25,
611      disbursementFee: 0.75,
612      netAmount: 740.5,
613      description: "Freelance payment",
614    },
615    {
616      id: "3",
617      transactionId: "TXN-2024-003",
618      amount: 2000.0,
619      currency: "USD",
620      destination: "Bank of America ****5678",
621      destinationType: "bank",
622      paymentDate: "2024-01-13T09:15:00Z",
623      disbursementDate: "",
624      paymentStatus: "failed",
625      disbursementStatus: "failed",
626      paymentProcessor: "Square",
627      platformFee: 20.0,
628      paymentFee: 3.0,
629      disbursementFee: 2.0,
630      netAmount: 1975.0,
631      description: "Refund payment",
632    },
633    {
634      id: "4",
635      transactionId: "TXN-2024-004",
636      amount: 500.0,
637      currency: "USD",
638      destination: "**** **** **** 9876",
639      destinationType: "card",
640      paymentDate: "2024-01-12T16:45:00Z",
641      disbursementDate: "2024-01-12T17:00:00Z",
642      paymentStatus: "completed",
643      disbursementStatus: "completed",
644      paymentProcessor: "Adyen",
645      platformFee: 5.0,
646      paymentFee: 1.0,
647      disbursementFee: 0.5,
648      netAmount: 493.5,
649      description: "Bonus payment",
650    },
651    {
652      id: "5",
653      transactionId: "TXN-2024-005",
654      amount: 1200.0,
655      currency: "USD",
656      destination: "crypto@wallet.com",
657      destinationType: "wallet",
658      paymentDate: "2024-01-11T12:30:00Z",
659      disbursementDate: "",
660      paymentStatus: "pending",
661      disbursementStatus: "pending",
662      paymentProcessor: "Coinbase",
663      platformFee: 12.0,
664      paymentFee: 2.0,
665      disbursementFee: 1.5,
666      netAmount: 1184.5,
667      description: "Investment return",
668    },
669  ];
670
671  const columns: DataTableColumn<MoneyTransferTransaction>[] = [
672    {
673      key: "transactionId",
674      header: "Transaction ID",
675      sortable: true,
676      filterable: true,
677      width: "150px",
678      render: (value) => (
679        <div className="font-mono text-sm font-medium">{value}</div>
680      ),
681    },
682    {
683      key: "amount",
684      header: "Amount",
685      sortable: true,
686      width: "120px",
687      render: (value, row) => (
688        <div className="font-semibold">
689          {formatCurrency(value, row.currency)}
690        </div>
691      ),
692    },
693    {
694      key: "destination",
695      header: "Destination",
696      filterable: true,
697      width: "200px",
698      render: (value, row) => (
699        <div className="flex items-center gap-2">
700          {row.destinationType === "bank" && (
701            <Building className="h-4 w-4 text-blue-500" />
702          )}
703          {row.destinationType === "card" && (
704            <CreditCard className="h-4 w-4 text-green-500" />
705          )}
706          {row.destinationType === "wallet" && (
707            <DollarSign className="h-4 w-4 text-purple-500" />
708          )}
709          <span className="truncate">{value}</span>
710        </div>
711      ),
712    },
713    {
714      key: "paymentDate",
715      header: "Payment Date",
716      sortable: true,
717      width: "180px",
718      render: (value) => (
719        <div className="flex items-center gap-2">
720          <Calendar className="h-4 w-4 text-muted-foreground" />
721          <span className="text-sm">{formatDate(value)}</span>
722        </div>
723      ),
724    },
725    {
726      key: "disbursementDate",
727      header: "Disbursement Date",
728      sortable: true,
729      width: "180px",
730      render: (value) => (
731        <div className="flex items-center gap-2">
732          <Calendar className="h-4 w-4 text-muted-foreground" />
733          <span className="text-sm">{value ? formatDate(value) : "N/A"}</span>
734        </div>
735      ),
736    },
737    {
738      key: "paymentStatus",
739      header: "Payment Status",
740      filterable: true,
741      width: "140px",
742      render: (value) => renderStatus(value),
743    },
744    {
745      key: "disbursementStatus",
746      header: "Disbursement Status",
747      filterable: true,
748      width: "160px",
749      render: (value) => renderStatus(value),
750    },
751    {
752      key: "paymentProcessor",
753      header: "Processor",
754      filterable: true,
755      width: "120px",
756      render: (value) => <Badge variant="outline">{value}</Badge>,
757    },
758    {
759      key: "platformFee",
760      header: "Platform Fee",
761      sortable: true,
762      width: "120px",
763      render: (value, row) => (
764        <div className="text-sm font-medium text-green-600">
765          +{formatCurrency(value, row.currency)}
766        </div>
767      ),
768    },
769    {
770      key: "netAmount",
771      header: "Net Amount",
772      sortable: true,
773      width: "120px",
774      render: (value, row) => (
775        <div className="font-semibold text-blue-600">
776          {formatCurrency(value, row.currency)}
777        </div>
778      ),
779    },
780    {
781      key: "id",
782      header: "Actions",
783      width: "80px",
784      render: (_, row) => (
785        <button
786          onClick={(e) => {
787            e.stopPropagation();
788            setSelectedTransaction(row);
789          }}
790          className="p-1 hover:bg-muted rounded transition-colors"
791        >
792          <Eye className="h-4 w-4 text-muted-foreground" />
793        </button>
794      ),
795    },
796  ];
797
798  return (
799    <div className="p-6 space-y-6">
800      <div className="space-y-2">
801        <h1 className="text-3xl font-bold text-foreground">
802          Money Transfer Transactions
803        </h1>
804        <p className="text-muted-foreground">
805          Track and manage all money transfer transactions with detailed payment
806          and disbursement information.
807        </p>
808      </div>
809
810      <DataTable
811        data={transactions}
812        columns={columns}
813        searchable
814        searchPlaceholder="Search by transaction ID, destination, or processor..."
815        itemsPerPage={10}
816        showPagination
817        hoverable
818        bordered
819        onRowClick={(row) => setSelectedTransaction(row)}
820        className="shadow-sm"
821      />
822
823      {/* Transaction Detail Modal */}
824      {selectedTransaction && (
825        <div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
826          <div className="bg-background rounded-lg border max-w-2xl w-full max-h-[90vh] overflow-y-auto">
827            <div className="p-6 border-b border-border">
828              <div className="flex items-center justify-between">
829                <h2 className="text-xl font-semibold">Transaction Details</h2>
830                <button
831                  onClick={() => setSelectedTransaction(null)}
832                  className="p-1 hover:bg-muted rounded transition-colors"
833                >
834                  <XCircle className="h-5 w-5" />
835                </button>
836              </div>
837            </div>
838
839            <div className="p-6 space-y-6">
840              <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
841                <div className="space-y-4">
842                  <h3 className="font-semibold text-lg">Transaction Info</h3>
843                  <div className="space-y-3">
844                    <div>
845                      <label className="text-sm font-medium text-muted-foreground">
846                        Transaction ID
847                      </label>
848                      <div className="font-mono text-sm">
849                        {selectedTransaction.transactionId}
850                      </div>
851                    </div>
852                    <div>
853                      <label className="text-sm font-medium text-muted-foreground">
854                        Amount
855                      </label>
856                      <div className="text-lg font-semibold">
857                        {formatCurrency(
858                          selectedTransaction.amount,
859                          selectedTransaction.currency
860                        )}
861                      </div>
862                    </div>
863                    <div>
864                      <label className="text-sm font-medium text-muted-foreground">
865                        Destination
866                      </label>
867                      <div className="flex items-center gap-2">
868                        {selectedTransaction.destinationType === "bank" && (
869                          <Building className="h-4 w-4 text-blue-500" />
870                        )}
871                        {selectedTransaction.destinationType === "card" && (
872                          <CreditCard className="h-4 w-4 text-green-500" />
873                        )}
874                        {selectedTransaction.destinationType === "wallet" && (
875                          <DollarSign className="h-4 w-4 text-purple-500" />
876                        )}
877                        {selectedTransaction.destination}
878                      </div>
879                    </div>
880                    <div>
881                      <label className="text-sm font-medium text-muted-foreground">
882                        Payment Processor
883                      </label>
884                      <div>
885                        <Badge variant="outline">
886                          {selectedTransaction.paymentProcessor}
887                        </Badge>
888                      </div>
889                    </div>
890                  </div>
891                </div>
892
893                <div className="space-y-4">
894                  <h3 className="font-semibold text-lg">Status & Timing</h3>
895                  <div className="space-y-3">
896                    <div>
897                      <label className="text-sm font-medium text-muted-foreground">
898                        Payment Status
899                      </label>
900                      <div>
901                        {renderStatus(selectedTransaction.paymentStatus)}
902                      </div>
903                    </div>
904                    <div>
905                      <label className="text-sm font-medium text-muted-foreground">
906                        Disbursement Status
907                      </label>
908                      <div>
909                        {renderStatus(selectedTransaction.disbursementStatus)}
910                      </div>
911                    </div>
912                    <div>
913                      <label className="text-sm font-medium text-muted-foreground">
914                        Payment Date
915                      </label>
916                      <div className="flex items-center gap-2">
917                        <Clock className="h-4 w-4 text-muted-foreground" />
918                        {formatDate(selectedTransaction.paymentDate)}
919                      </div>
920                    </div>
921                    <div>
922                      <label className="text-sm font-medium text-muted-foreground">
923                        Disbursement Date
924                      </label>
925                      <div className="flex items-center gap-2">
926                        <Clock className="h-4 w-4 text-muted-foreground" />
927                        {selectedTransaction.disbursementDate
928                          ? formatDate(selectedTransaction.disbursementDate)
929                          : "Pending"}
930                      </div>
931                    </div>
932                  </div>
933                </div>
934              </div>
935
936              <div className="space-y-4">
937                <h3 className="font-semibold text-lg">Fee Breakdown</h3>
938                <div className="bg-muted/30 rounded-lg p-4 space-y-3">
939                  <div className="flex justify-between">
940                    <span>Original Amount:</span>
941                    <span className="font-medium">
942                      {formatCurrency(
943                        selectedTransaction.amount,
944                        selectedTransaction.currency
945                      )}
946                    </span>
947                  </div>
948                  <div className="flex justify-between text-green-600">
949                    <span>Platform Fee (Revenue):</span>
950                    <span className="font-medium">
951                      +
952                      {formatCurrency(
953                        selectedTransaction.platformFee,
954                        selectedTransaction.currency
955                      )}
956                    </span>
957                  </div>
958                  <div className="flex justify-between text-red-600">
959                    <span>Payment Fee:</span>
960                    <span className="font-medium">
961                      -
962                      {formatCurrency(
963                        selectedTransaction.paymentFee,
964                        selectedTransaction.currency
965                      )}
966                    </span>
967                  </div>
968                  <div className="flex justify-between text-red-600">
969                    <span>Disbursement Fee:</span>
970                    <span className="font-medium">
971                      -
972                      {formatCurrency(
973                        selectedTransaction.disbursementFee,
974                        selectedTransaction.currency
975                      )}
976                    </span>
977                  </div>
978                  <div className="border-t border-border pt-3 flex justify-between text-lg font-semibold">
979                    <span>Net Amount:</span>
980                    <span className="text-blue-600">
981                      {formatCurrency(
982                        selectedTransaction.netAmount,
983                        selectedTransaction.currency
984                      )}
985                    </span>
986                  </div>
987                </div>
988              </div>
989
990              {selectedTransaction.description && (
991                <div className="space-y-2">
992                  <h3 className="font-semibold">Description</h3>
993                  <p className="text-muted-foreground">
994                    {selectedTransaction.description}
995                  </p>
996                </div>
997              )}
998            </div>
999          </div>
1000        </div>
1001      )}
1002    </div>
1003  );
1004}
1005
1006export default MoneyTransferTransactionList;

Dependencies

External Libraries

class-variance-authoritylucide-reactreact

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.