PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
3.0.17 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 All 85 releases
yatra / resources / js / pages / account / Payments.tsx

Payments.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.2.9, at resources/js/pages/account/Payments.tsx

388 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React from "react";
2 import {
3 CreditCard,
4 CheckCircle,
5 Clock as ClockIcon,
6 DollarSign as DollarSignIcon,
7 Calendar as CalendarIcon,
8 FileText as FileTextIcon,
9 Eye,
10 Download,
11 } from "lucide-react";
12 import { __ } from "../../lib/i18n";
13 import { formatDate, getBadge, currency } from "./utils";
14 import { downloadInvoice, previewPaymentInvoice } from "./utils/downloads";
15 import type { Payment } from "./types";
16
17 interface PaymentsProps {
18 payments: Payment[];
19 onSectionChange: (section: string) => void;
20 }
21
22 const Payments: React.FC<PaymentsProps> = ({ payments, onSectionChange }) => {
23 const displayPayments = payments;
24
25 const bookingSummaries = React.useMemo(() => {
26 const map = new Map<number, { total: number; paid: number; due: number }>();
27
28 displayPayments.forEach((payment) => {
29 if (typeof payment.booking_id !== "number") {
30 return;
31 }
32 const total =
33 typeof payment.booking_total_amount === "number"
34 ? payment.booking_total_amount
35 : null;
36 const dueValue =
37 typeof payment.booking_amount_due === "number"
38 ? payment.booking_amount_due
39 : null;
40 const paidValue =
41 typeof payment.booking_amount_paid === "number"
42 ? payment.booking_amount_paid
43 : null;
44 const paid =
45 paidValue ??
46 (total !== null && dueValue !== null ? total - dueValue : null);
47 const due =
48 dueValue ?? (total !== null && paid !== null ? total - paid : null);
49 const existing = map.get(payment.booking_id);
50 const summary = {
51 total: total ?? existing?.total ?? 0,
52 paid: paid ?? existing?.paid ?? 0,
53 due: due ?? existing?.due ?? 0,
54 };
55
56 // Prefer the latest (smallest due) snapshot
57 if (!existing || summary.due < existing.due) {
58 map.set(payment.booking_id, summary);
59 }
60 });
61
62 return Array.from(map.values());
63 }, [displayPayments]);
64
65 const paymentStats = React.useMemo(() => {
66 const totals = bookingSummaries.reduce(
67 (acc, summary) => {
68 acc.totalAmount += summary.total;
69 acc.paidAmount += summary.total - summary.due;
70 acc.outstanding += summary.due;
71 if (summary.due > 0) {
72 acc.pendingBookings += 1;
73 } else {
74 acc.paidBookings += 1;
75 }
76 return acc;
77 },
78 {
79 totalAmount: 0,
80 paidAmount: 0,
81 outstanding: 0,
82 pendingBookings: 0,
83 paidBookings: 0,
84 },
85 );
86
87 return {
88 total: displayPayments.length,
89 paid: totals.paidBookings,
90 pending: totals.pendingBookings,
91 totalPaidAmount: totals.paidAmount,
92 outstandingAmount: totals.outstanding,
93 };
94 }, [displayPayments, bookingSummaries]);
95
96 return (
97 <div className="yatra-payments-page space-y-6">
98 {/* Header */}
99 <div className="yatra-payments-header bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
100 <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
101 <div>
102 <h2 className="text-lg font-bold text-gray-900 dark:text-white flex items-center gap-3">
103 <div className="p-2 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg">
104 <CreditCard className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
105 </div>
106 {__("Payments & Invoices", "yatra")}
107 </h2>
108 <p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
109 {__(
110 "Track deposits, balances, and installment schedules with secure payment links.",
111 "yatra",
112 )}
113 </p>
114 </div>
115 </div>
116 </div>
117
118 {/* Payment Statistics */}
119 <div className="yatra-payments-stats grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
120 <div className="yatra-payment-stat-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-4">
121 <div className="flex items-center justify-between">
122 <div>
123 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
124 {__("Total Payments", "yatra")}
125 </p>
126 <p className="text-xl font-bold text-gray-900 dark:text-white">
127 {paymentStats.total}
128 </p>
129 </div>
130 <div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
131 <CreditCard className="w-5 h-5 text-blue-600 dark:text-blue-400" />
132 </div>
133 </div>
134 </div>
135 <div className="yatra-payment-stat-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-4">
136 <div className="flex items-center justify-between">
137 <div>
138 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
139 {__("Paid", "yatra")}
140 </p>
141 <p className="text-xl font-bold text-emerald-600 dark:text-emerald-400">
142 {paymentStats.paid}
143 </p>
144 <p className="text-xs text-gray-400 dark:text-gray-500">
145 {currency(paymentStats.totalPaidAmount)}
146 </p>
147 </div>
148 <div className="p-2 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg">
149 <CheckCircle className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
150 </div>
151 </div>
152 </div>
153 <div className="yatra-payment-stat-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-4">
154 <div className="flex items-center justify-between">
155 <div>
156 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
157 {__("Pending", "yatra")}
158 </p>
159 <p className="text-xl font-bold text-amber-600 dark:text-amber-400">
160 {paymentStats.pending}
161 </p>
162 </div>
163 <div className="p-2 bg-amber-50 dark:bg-amber-900/20 rounded-lg">
164 <ClockIcon className="w-5 h-5 text-amber-600 dark:text-amber-400" />
165 </div>
166 </div>
167 </div>
168 <div className="yatra-payment-stat-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-4">
169 <div className="flex items-center justify-between">
170 <div>
171 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
172 {__("Outstanding", "yatra")}
173 </p>
174 <p className="text-xl font-bold text-gray-900 dark:text-white">
175 {currency(paymentStats.outstandingAmount)}
176 </p>
177 </div>
178 <div className="p-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
179 <DollarSignIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
180 </div>
181 </div>
182 </div>
183 </div>
184
185 {/* Payments List */}
186 {displayPayments.length === 0 ? (
187 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-12 text-center">
188 <CreditCard className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
189 <p className="text-gray-500 dark:text-gray-400 font-medium mb-1">
190 {__("No payments found", "yatra")}
191 </p>
192 <p className="text-sm text-gray-400 dark:text-gray-500">
193 {__(
194 "Payment history will appear here once you make a booking.",
195 "yatra",
196 )}
197 </p>
198 </div>
199 ) : (
200 <div className="space-y-4">
201 {displayPayments.map((payment) => {
202 const bookingTotal =
203 typeof payment.booking_total_amount === "number"
204 ? payment.booking_total_amount
205 : null;
206 const bookingPaid =
207 typeof payment.booking_amount_paid === "number"
208 ? payment.booking_amount_paid
209 : null;
210 const bookingDue =
211 typeof payment.booking_amount_due === "number"
212 ? payment.booking_amount_due
213 : null;
214 const total = bookingTotal ?? payment.amount;
215 const paid =
216 bookingPaid ??
217 (bookingDue !== null && bookingTotal !== null
218 ? bookingTotal - bookingDue
219 : payment.amount);
220 const dueRaw =
221 bookingDue ??
222 (bookingTotal !== null ? bookingTotal - paid : total - paid);
223 const due = Math.max(0, dueRaw || 0);
224 const canPayRemaining =
225 typeof payment.booking_id === "number" && due > 0.01;
226 const isPaid =
227 payment.status === "paid" || payment.status === "completed";
228 const isPending = payment.status === "pending";
229
230 return (
231 <div
232 key={payment.id}
233 className="group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all overflow-hidden"
234 >
235 <div className="p-6">
236 {/* Header Row */}
237 <div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 mb-4">
238 <div className="flex-1">
239 <div className="flex items-center gap-3 mb-2">
240 <div
241 className={`p-2 rounded-lg ${
242 payment.status === "paid"
243 ? "bg-emerald-50 dark:bg-emerald-900/20"
244 : payment.status === "pending"
245 ? "bg-amber-50 dark:bg-amber-900/20"
246 : "bg-gray-50 dark:bg-gray-700"
247 }`}
248 >
249 <CreditCard
250 className={`w-5 h-5 ${
251 payment.status === "paid"
252 ? "text-emerald-600 dark:text-emerald-400"
253 : payment.status === "pending"
254 ? "text-amber-600 dark:text-amber-400"
255 : "text-gray-400"
256 }`}
257 />
258 </div>
259 <div className="flex-1">
260 <p className="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400 font-medium mb-1">
261 {payment.reference}
262 </p>
263 <h3 className="text-base font-bold text-gray-900 dark:text-white mb-1">
264 {__("Booking", "yatra")}: {payment.booking_number}
265 </h3>
266 <p className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
267 <CalendarIcon className="w-4 h-4" />
268 {formatDate(payment.date)}
269 </p>
270 </div>
271 </div>
272 </div>
273 <div className="flex flex-wrap gap-2 items-start">
274 <span className={getBadge(payment.status)}>
275 {__(payment.status, payment.status)}
276 </span>
277 <span className="px-3 py-1 rounded-lg text-xs font-medium bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 capitalize">
278 {payment.type}
279 </span>
280 </div>
281 </div>
282
283 {/* Details Grid */}
284 <div className="yatra-payment-details grid grid-cols-2 md:grid-cols-3 gap-4 p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg mb-4">
285 <div>
286 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1 flex items-center gap-1">
287 <DollarSignIcon className="w-3.5 h-3.5" />
288 {__("Amount", "yatra")}
289 </p>
290 <p className="text-sm font-semibold text-gray-900 dark:text-white">
291 {currency(payment.amount)}
292 </p>
293 </div>
294 <div>
295 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1 flex items-center gap-1">
296 <CreditCard className="w-3.5 h-3.5" />
297 {__("Payment Method", "yatra")}
298 </p>
299 <p className="text-sm font-semibold text-gray-900 dark:text-white">
300 {payment.method}
301 </p>
302 </div>
303 <div>
304 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1 flex items-center gap-1">
305 <CalendarIcon className="w-3.5 h-3.5" />
306 {__("Payment Date", "yatra")}
307 </p>
308 <p className="text-sm font-semibold text-gray-900 dark:text-white">
309 {formatDate(payment.date)}
310 </p>
311 </div>
312 </div>
313
314 {/* Action Buttons */}
315 <div className="yatra-payment-actions flex flex-wrap gap-3">
316 {/* Download Invoice - show for paid/completed payments */}
317 {isPaid && (
318 <button
319 type="button"
320 onClick={async () => {
321 try {
322 await downloadInvoice(payment.id);
323 } catch (err) {
324 alert(
325 err instanceof Error
326 ? err.message
327 : __("Download failed.", "yatra"),
328 );
329 }
330 }}
331 className="yatra-payment-action yatra-payment-action-invoice inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium"
332 style={{ backgroundColor: "#059669", color: "#ffffff" }}
333 >
334 <Download className="w-4 h-4" />
335 {__("Download Invoice", "yatra")}
336 </button>
337 )}
338 {isPaid && (
339 <button
340 type="button"
341 onClick={async () => {
342 try {
343 await previewPaymentInvoice(payment.id);
344 } catch (err) {
345 alert(
346 err instanceof Error
347 ? err.message
348 : __("Could not open receipt.", "yatra"),
349 );
350 }
351 }}
352 className="yatra-payment-action yatra-payment-action-receipt inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium"
353 style={{ backgroundColor: "#2563eb", color: "#ffffff" }}
354 >
355 <FileTextIcon className="w-4 h-4" />
356 {__("View Receipt", "yatra")}
357 </button>
358 )}
359 {isPending && !canPayRemaining && (
360 <button
361 type="button"
362 className="yatra-payment-action inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-amber-600 text-white text-sm font-medium"
363 >
364 <ClockIcon className="w-4 h-4" />
365 {__("Pending approval", "yatra")}
366 </button>
367 )}
368 <button
369 type="button"
370 onClick={() => onSectionChange("bookings")}
371 className="yatra-payment-action yatra-payment-action-booking inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium"
372 >
373 <Eye className="w-4 h-4" />
374 {__("View Booking Details", "yatra")}
375 </button>
376 </div>
377 </div>
378 </div>
379 );
380 })}
381 </div>
382 )}
383 </div>
384 );
385 };
386
387 export default Payments;
388