PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / components / dashboard / PendingPayments.tsx

PendingPayments.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.13, at resources/js/components/dashboard/PendingPayments.tsx

172 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Pending Payments Widget
3 * Displays bookings with pending payments
4 */
5
6 import React from "react";
7 import { __ } from "../../lib/i18n";
8 import { toDateValue } from "../../lib/dateFormat";
9 import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
10 import { Badge } from "../ui/badge";
11 import { Button } from "../ui/button";
12 import { DollarSign, Clock, User } from "lucide-react";
13 import { ConditionalRender } from "../ui/conditional-render";
14 import { formatYatraMoney } from "../../lib/currency-display";
15
16 interface PendingPayment {
17 id: number;
18 booking_id: number;
19 customer_name: string;
20 trip_title: string;
21 amount: number;
22 due_date?: string | null;
23 days_overdue?: number;
24 payment_method?: string;
25 payment_method_label?: string;
26 }
27
28 interface PendingPaymentsProps {
29 payments: PendingPayment[];
30 loading?: boolean;
31 onView?: (payment: PendingPayment) => void;
32 onCollect?: (payment: PendingPayment) => void;
33 }
34
35 /**
36 * Pending Payments Widget
37 */
38 export const PendingPayments: React.FC<PendingPaymentsProps> = ({
39 payments,
40 loading = false,
41 onView,
42 onCollect,
43 }) => {
44 const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD";
45 const formatCurrency = (amount: number) =>
46 formatYatraMoney(Number(amount) || 0, globalCurrency, {
47 zeroAsUnknown: false,
48 });
49
50 const formatDate = (dateString?: string | null) => {
51 // toDateValue: a date-only due_date must not roll back a day in behind-UTC zones.
52 // A payment can arrive with no due date, so render a dash rather than throwing.
53 const date = toDateValue(dateString);
54 if (isNaN(date.getTime())) return "-";
55 return date.toLocaleDateString();
56 };
57
58 const totalPending = payments?.reduce((sum, p) => sum + p.amount, 0) || 0;
59
60 if (loading) {
61 return (
62 <Card>
63 <CardHeader>
64 <CardTitle>{__("Pending Payments", "yatra")}</CardTitle>
65 </CardHeader>
66 <CardContent>
67 <div className="text-sm text-gray-500 dark:text-gray-400">
68 {__("Loading...", "yatra")}
69 </div>
70 </CardContent>
71 </Card>
72 );
73 }
74
75 return (
76 <Card>
77 <CardHeader>
78 <div className="flex items-center justify-between">
79 <CardTitle>{__("Pending Payments", "yatra")}</CardTitle>
80 {totalPending > 0 && (
81 <Badge variant="warning" className="text-sm">
82 {formatCurrency(totalPending)}
83 </Badge>
84 )}
85 </div>
86 </CardHeader>
87 <CardContent>
88 {payments && payments.length > 0 ? (
89 <div className="space-y-2">
90 {payments.slice(0, 2).map((payment) => (
91 <div
92 key={payment.id}
93 className="p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
94 >
95 <div className="flex items-start justify-between mb-2">
96 <div className="flex-1">
97 <div className="flex items-center gap-2 mb-1">
98 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
99 {payment.trip_title}
100 </h4>
101 {payment.days_overdue && payment.days_overdue > 0 && (
102 <Badge variant="error" className="text-xs">
103 {payment.days_overdue} {__("days overdue", "yatra")}
104 </Badge>
105 )}
106 </div>
107 <div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400 mb-1">
108 <User className="w-3 h-3" />
109 <span>{payment.customer_name}</span>
110 </div>
111 {/* Payments carry no due date today, so this line stays hidden
112 rather than printing an empty "Due:" for every row. */}
113 {formatDate(payment.due_date) !== "-" && (
114 <div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
115 <Clock className="w-3 h-3" />
116 <span>
117 {__("Due", "yatra")}: {formatDate(payment.due_date)}
118 </span>
119 </div>
120 )}
121 </div>
122 <div className="text-right">
123 <p className="text-base font-semibold text-gray-900 dark:text-white">
124 {formatCurrency(payment.amount)}
125 </p>
126 {payment.payment_method && (
127 <p className="text-xs text-gray-500 dark:text-gray-400">
128 {payment.payment_method_label || payment.payment_method}
129 </p>
130 )}
131 </div>
132 </div>
133
134 <ConditionalRender capability="yatra_edit_bookings">
135 <div className="flex gap-2 mt-2">
136 {onView && (
137 <Button
138 variant="outline"
139 size="sm"
140 onClick={() => onView(payment)}
141 className="flex-1"
142 >
143 {__("View", "yatra")}
144 </Button>
145 )}
146 {onCollect && (
147 <Button
148 size="sm"
149 onClick={() => onCollect(payment)}
150 className="flex-1"
151 >
152 <DollarSign className="w-4 h-4 mr-1" />
153 {__("Collect", "yatra")}
154 </Button>
155 )}
156 </div>
157 </ConditionalRender>
158 </div>
159 ))}
160 </div>
161 ) : (
162 <p className="text-sm text-gray-500 dark:text-gray-400">
163 {__("No pending payments", "yatra")}
164 </p>
165 )}
166 </CardContent>
167 </Card>
168 );
169 };
170
171 export default PendingPayments;
172