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 / pages / account / Payments.tsx

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

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