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 >
383 ✕
384 </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