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 >
393 ✕
394 </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