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 / ViewPayment.tsx

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

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