PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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.3, at resources/js/pages/ViewPayment.tsx

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