ShadcnUI Vaults

Back to Blocks

Enhanced Transaction Dashboard

Unknown Block

UnknownComponent

Enhanced Transaction Dashboard

Advanced transaction dashboard with status badges, revenue tracking, and comprehensive money transfer management.

Preview

Full width desktop view

Code

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