PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / ViewPayment.tsx

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

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