PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
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 / ViewPayment.tsx

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

535 lines 18.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * View Payment Page
3 * Display payment details in a clean, minimal SaaS-style design
4 */
5
6 import React, { useMemo } from "react";
7 import { useQuery } from "@tanstack/react-query";
8 import {
9 ArrowLeft,
10 Mail,
11 Phone,
12 Calendar,
13 CreditCard,
14 Edit,
15 ExternalLink,
16 Download,
17 } from "lucide-react";
18 import { __ } from "../lib/i18n";
19 import { apiService } from "../lib/api-client";
20 import { downloadAdminInvoice } from "../lib/invoice-download";
21 import { useToast } from "../components/ui/toast";
22 import { usePermissions } from "../hooks/usePermissions";
23 import { Button } from "../components/ui/button";
24 import { PageHeader } from "../components/common/PageHeader";
25 import {
26 Card,
27 CardContent,
28 CardHeader,
29 CardTitle,
30 } from "../components/ui/card";
31 import { ConditionalRender } from "../components/ui/conditional-render";
32 import { Skeleton } from "../components/ui/skeleton";
33 import { getCurrencySymbol, getCurrency } from "../data/currencies";
34
35 function paymentStr(v: unknown, fallback = ""): string {
36 if (v == null || v === "") return fallback;
37 return String(v);
38 }
39
40 function paymentNum(v: unknown): number {
41 if (typeof v === "number" && !Number.isNaN(v)) return v;
42 const n = typeof v === "string" ? parseFloat(v) : NaN;
43 return Number.isNaN(n) ? 0 : n;
44 }
45
46 interface ViewPaymentRow {
47 id: number;
48 payment_number: string;
49 booking_id: number;
50 booking_number: string;
51 customer_name: string;
52 customer_email: string;
53 customer_phone: string;
54 trip_id: number;
55 trip_title: string;
56 amount: number;
57 payment_method: string;
58 payment_status: string;
59 transaction_id: string;
60 payment_date: string;
61 notes: string;
62 created_at: string;
63 updated_at: string;
64 }
65
66 const ViewPayment: React.FC = () => {
67 const { can } = usePermissions();
68 const { showToast } = useToast();
69 const [isDownloadingInvoice, setIsDownloadingInvoice] = React.useState(false);
70
71 // Get payment id from URL
72 const paymentId = useMemo(() => {
73 const params = new URLSearchParams(window.location.search);
74 return params.get("id") ? parseInt(params.get("id") || "0") : null;
75 }, []);
76
77 // Fetch payment data
78 const {
79 data: payment,
80 isLoading,
81 error,
82 } = useQuery({
83 queryKey: ["payment", paymentId],
84 queryFn: async () => {
85 if (!paymentId) return null;
86
87 const result = await apiService.getPayment(paymentId);
88
89 if (!result) {
90 throw new Error("Failed to fetch payment");
91 }
92
93 if (!result.success) {
94 throw new Error(result.message || "Payment not found");
95 }
96
97 const data = result.data;
98 const idNum = paymentNum(data.id);
99 const bookingId = paymentNum(data.booking_id);
100 const ref = paymentStr(data.booking_reference);
101 const row: ViewPaymentRow = {
102 id: idNum,
103 payment_number: `PAY-${String(idNum).padStart(6, "0")}`,
104 booking_id: bookingId,
105 booking_number: ref || `#${bookingId}`,
106 customer_name: paymentStr(data.customer_name, "N/A"),
107 customer_email: paymentStr(data.customer_email),
108 customer_phone: paymentStr(data.customer_phone),
109 trip_id: paymentNum(data.trip_id),
110 trip_title: paymentStr(data.trip_title),
111 amount: paymentNum(data.amount),
112 payment_method: paymentStr(data.gateway),
113 payment_status: paymentStr(data.status, "pending"),
114 transaction_id: paymentStr(data.transaction_id),
115 payment_date: paymentStr(data.processed_at || data.created_at),
116 notes: paymentStr(data.notes),
117 created_at: paymentStr(data.created_at),
118 updated_at: paymentStr(data.updated_at),
119 };
120 return row;
121 },
122 enabled: !!paymentId && can("yatra_view_bookings"),
123 });
124
125 const formatDate = (dateString: string) => {
126 return new Date(dateString).toLocaleDateString("en-US", {
127 weekday: "long",
128 year: "numeric",
129 month: "long",
130 day: "numeric",
131 });
132 };
133
134 const formatPrice = (price: number, currencyCode: string = "USD") => {
135 const symbol = getCurrencySymbol(currencyCode);
136 const currencyData = getCurrency(currencyCode);
137 const decimals = currencyData?.decimalDigits ?? 2;
138
139 return `${symbol}${new Intl.NumberFormat(undefined, {
140 minimumFractionDigits: decimals,
141 maximumFractionDigits: decimals,
142 }).format(price)}`;
143 };
144
145 const getPaymentStatusBadge = (status: string) => {
146 const statusMap: Record<string, { className: string; label: string }> = {
147 completed: {
148 className:
149 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
150 label: __("Completed", "yatra"),
151 },
152 pending: {
153 className:
154 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
155 label: __("Pending", "yatra"),
156 },
157 partial: {
158 className:
159 "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
160 label: __("Partial", "yatra"),
161 },
162 failed: {
163 className:
164 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
165 label: __("Failed", "yatra"),
166 },
167 refunded: {
168 className:
169 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
170 label: __("Refunded", "yatra"),
171 },
172 };
173
174 const statusInfo = statusMap[status] || {
175 className:
176 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
177 label: status,
178 };
179
180 return (
181 <span
182 className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${statusInfo.className}`}
183 >
184 {statusInfo.label}
185 </span>
186 );
187 };
188
189 const handleBack = () => {
190 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=payments`;
191 };
192
193 const handleEdit = () => {
194 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=payments&action=edit&id=${paymentId}`;
195 };
196
197 const handleViewBooking = () => {
198 if (payment?.booking_id) {
199 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings&action=view&id=${payment.booking_id}`;
200 }
201 };
202
203 const handleDownloadInvoice = async () => {
204 if (!paymentId) return;
205 setIsDownloadingInvoice(true);
206 try {
207 await downloadAdminInvoice(paymentId);
208 } catch (e: any) {
209 showToast(
210 e?.message || __("Failed to download invoice", "yatra"),
211 "error",
212 );
213 } finally {
214 setIsDownloadingInvoice(false);
215 }
216 };
217
218 const canDownloadInvoice =
219 !!payment &&
220 ["completed", "paid"].includes(
221 String(payment.payment_status).toLowerCase(),
222 );
223
224 if (isLoading) {
225 return (
226 <div className="space-y-3">
227 <PageHeader
228 title={__("Payment Details", "yatra")}
229 actions={
230 <Button
231 variant="outline"
232 onClick={handleBack}
233 className="flex items-center gap-2"
234 >
235 <ArrowLeft className="w-4 h-4" />
236 {__("Back", "yatra")}
237 </Button>
238 }
239 />
240 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
241 <div className="lg:col-span-2 space-y-3">
242 <Card>
243 <CardHeader className="pb-2">
244 <Skeleton className="h-6 w-40" />
245 </CardHeader>
246 <CardContent className="space-y-4">
247 <div className="grid grid-cols-2 gap-4">
248 <Skeleton className="h-12 w-full" />
249 <Skeleton className="h-12 w-full" />
250 <Skeleton className="h-12 w-full" />
251 <Skeleton className="h-12 w-full" />
252 </div>
253 </CardContent>
254 </Card>
255 <Card>
256 <CardHeader className="pb-2">
257 <Skeleton className="h-6 w-32" />
258 </CardHeader>
259 <CardContent>
260 <div className="grid grid-cols-2 gap-4">
261 <Skeleton className="h-12 w-full" />
262 <Skeleton className="h-12 w-full" />
263 </div>
264 </CardContent>
265 </Card>
266 </div>
267 <div className="space-y-3">
268 <Card>
269 <CardHeader className="pb-2">
270 <Skeleton className="h-6 w-24" />
271 </CardHeader>
272 <CardContent className="space-y-3">
273 <Skeleton className="h-5 w-full" />
274 <Skeleton className="h-5 w-3/4" />
275 <Skeleton className="h-5 w-1/2" />
276 </CardContent>
277 </Card>
278 </div>
279 </div>
280 </div>
281 );
282 }
283
284 if (error || !payment) {
285 return (
286 <div className="space-y-3">
287 <PageHeader
288 title={__("Payment Not Found", "yatra")}
289 actions={
290 <Button
291 variant="outline"
292 onClick={handleBack}
293 className="flex items-center gap-2"
294 >
295 <ArrowLeft className="w-4 h-4" />
296 {__("Back to Payments", "yatra")}
297 </Button>
298 }
299 />
300 <Card>
301 <CardContent className="p-8 text-center text-red-500">
302 {__(
303 "Payment not found or you do not have permission to view it.",
304 "yatra",
305 )}
306 </CardContent>
307 </Card>
308 </div>
309 );
310 }
311
312 return (
313 <div className="space-y-3">
314 <PageHeader
315 title={__("Payment Details", "yatra")}
316 description={__("View complete payment information", "yatra")}
317 actions={
318 <div className="flex items-center gap-2">
319 <Button
320 variant="outline"
321 onClick={handleBack}
322 className="flex items-center gap-2"
323 >
324 <ArrowLeft className="w-4 h-4" />
325 {__("Back", "yatra")}
326 </Button>
327 {canDownloadInvoice && (
328 <Button
329 variant="outline"
330 onClick={handleDownloadInvoice}
331 disabled={isDownloadingInvoice}
332 className="flex items-center gap-2"
333 >
334 <Download className="w-4 h-4" />
335 {isDownloadingInvoice
336 ? __("Downloading…", "yatra")
337 : __("Download Invoice", "yatra")}
338 </Button>
339 )}
340 <ConditionalRender capability="yatra_edit_bookings">
341 <Button onClick={handleEdit} className="flex items-center gap-2">
342 <Edit className="w-4 h-4" />
343 {__("Edit Payment", "yatra")}
344 </Button>
345 </ConditionalRender>
346 </div>
347 }
348 />
349
350 <ConditionalRender capability="yatra_view_bookings">
351 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
352 {/* Main Content */}
353 <div className="lg:col-span-2 space-y-3">
354 {/* Payment Information */}
355 <Card>
356 <CardHeader className="pb-2">
357 <div className="flex items-center justify-between">
358 <CardTitle className="text-base">
359 {__("Payment Information", "yatra")}
360 </CardTitle>
361 {getPaymentStatusBadge(payment.payment_status)}
362 </div>
363 </CardHeader>
364 <CardContent className="space-y-4">
365 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
366 <div>
367 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
368 {__("Payment Number", "yatra")}
369 </label>
370 <p className="mt-1 text-sm font-mono font-semibold text-gray-900 dark:text-white">
371 {payment.payment_number}
372 </p>
373 </div>
374 <div>
375 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
376 {__("Payment Amount", "yatra")}
377 </label>
378 <p className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
379 {formatPrice(payment.amount)}
380 </p>
381 </div>
382 <div>
383 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
384 {__("Payment Method", "yatra")}
385 </label>
386 <div className="mt-1 flex items-center gap-2">
387 <CreditCard className="w-4 h-4 text-gray-400" />
388 <p className="text-sm font-medium text-gray-900 dark:text-white">
389 {payment.payment_method}
390 </p>
391 </div>
392 </div>
393 <div>
394 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
395 {__("Payment Date", "yatra")}
396 </label>
397 <div className="mt-1 flex items-center gap-2">
398 <Calendar className="w-4 h-4 text-gray-400" />
399 <p className="text-sm font-medium text-gray-900 dark:text-white">
400 {formatDate(payment.payment_date)}
401 </p>
402 </div>
403 </div>
404 {payment.transaction_id && (
405 <div>
406 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
407 {__("Transaction ID", "yatra")}
408 </label>
409 <p className="mt-1 text-sm font-mono text-gray-900 dark:text-white">
410 {payment.transaction_id}
411 </p>
412 </div>
413 )}
414 </div>
415
416 {payment.notes && (
417 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
418 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2 block">
419 {__("Notes", "yatra")}
420 </label>
421 <p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
422 {payment.notes}
423 </p>
424 </div>
425 )}
426 </CardContent>
427 </Card>
428
429 {/* Booking Information */}
430 <Card>
431 <CardHeader className="pb-2">
432 <div className="flex items-center justify-between">
433 <CardTitle className="text-base">
434 {__("Related Booking", "yatra")}
435 </CardTitle>
436 <Button
437 variant="ghost"
438 size="sm"
439 onClick={handleViewBooking}
440 className="flex items-center gap-1.5 text-sm"
441 >
442 {__("View Booking", "yatra")}
443 <ExternalLink className="w-3.5 h-3.5" />
444 </Button>
445 </div>
446 </CardHeader>
447 <CardContent className="space-y-4">
448 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
449 <div>
450 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
451 {__("Booking Number", "yatra")}
452 </label>
453 <p className="mt-1 text-sm font-mono font-semibold text-gray-900 dark:text-white">
454 {payment.booking_number}
455 </p>
456 </div>
457 <div>
458 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
459 {__("Trip", "yatra")}
460 </label>
461 <p className="mt-1 text-sm font-medium text-gray-900 dark:text-white">
462 {payment.trip_title}
463 </p>
464 </div>
465 </div>
466 </CardContent>
467 </Card>
468 </div>
469
470 {/* Sidebar */}
471 <div className="space-y-3">
472 {/* Customer Information */}
473 <Card>
474 <CardHeader className="pb-2">
475 <CardTitle className="text-base">
476 {__("Customer", "yatra")}
477 </CardTitle>
478 </CardHeader>
479 <CardContent className="space-y-3">
480 <div>
481 <p className="text-sm font-medium text-gray-900 dark:text-white">
482 {payment.customer_name}
483 </p>
484 </div>
485 <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
486 <Mail className="w-4 h-4" />
487 <span>{payment.customer_email}</span>
488 </div>
489 {payment.customer_phone && (
490 <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
491 <Phone className="w-4 h-4" />
492 <span>{payment.customer_phone}</span>
493 </div>
494 )}
495 </CardContent>
496 </Card>
497
498 {/* Payment Timeline */}
499 <Card>
500 <CardHeader className="pb-2">
501 <CardTitle className="text-base">
502 {__("Timeline", "yatra")}
503 </CardTitle>
504 </CardHeader>
505 <CardContent className="space-y-3">
506 <div>
507 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
508 {__("Created", "yatra")}
509 </label>
510 <p className="mt-1 text-sm text-gray-900 dark:text-white">
511 {formatDate(payment.created_at)}
512 </p>
513 </div>
514 {payment.updated_at &&
515 payment.updated_at !== payment.created_at && (
516 <div>
517 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
518 {__("Last Updated", "yatra")}
519 </label>
520 <p className="mt-1 text-sm text-gray-900 dark:text-white">
521 {formatDate(payment.updated_at)}
522 </p>
523 </div>
524 )}
525 </CardContent>
526 </Card>
527 </div>
528 </div>
529 </ConditionalRender>
530 </div>
531 );
532 };
533
534 export default ViewPayment;
535