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

496 lines 17.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 } from "lucide-react";
17 import { __ } from "../lib/i18n";
18 import { apiService } from "../lib/api-client";
19 import { usePermissions } from "../hooks/usePermissions";
20 import { Button } from "../components/ui/button";
21 import { PageHeader } from "../components/common/PageHeader";
22 import {
23 Card,
24 CardContent,
25 CardHeader,
26 CardTitle,
27 } from "../components/ui/card";
28 import { ConditionalRender } from "../components/ui/conditional-render";
29 import { Skeleton } from "../components/ui/skeleton";
30 import { getCurrencySymbol, getCurrency } from "../data/currencies";
31
32 function paymentStr(v: unknown, fallback = ""): string {
33 if (v == null || v === "") return fallback;
34 return String(v);
35 }
36
37 function paymentNum(v: unknown): number {
38 if (typeof v === "number" && !Number.isNaN(v)) return v;
39 const n = typeof v === "string" ? parseFloat(v) : NaN;
40 return Number.isNaN(n) ? 0 : n;
41 }
42
43 interface ViewPaymentRow {
44 id: number;
45 payment_number: string;
46 booking_id: number;
47 booking_number: string;
48 customer_name: string;
49 customer_email: string;
50 customer_phone: string;
51 trip_id: number;
52 trip_title: string;
53 amount: number;
54 payment_method: string;
55 payment_status: string;
56 transaction_id: string;
57 payment_date: string;
58 notes: string;
59 created_at: string;
60 updated_at: string;
61 }
62
63 const ViewPayment: React.FC = () => {
64 const { can } = usePermissions();
65
66 // Get payment id from URL
67 const paymentId = useMemo(() => {
68 const params = new URLSearchParams(window.location.search);
69 return params.get("id") ? parseInt(params.get("id") || "0") : null;
70 }, []);
71
72 // Fetch payment data
73 const {
74 data: payment,
75 isLoading,
76 error,
77 } = useQuery({
78 queryKey: ["payment", paymentId],
79 queryFn: async () => {
80 if (!paymentId) return null;
81
82 const result = await apiService.getPayment(paymentId);
83
84 if (!result) {
85 throw new Error("Failed to fetch payment");
86 }
87
88 if (!result.success) {
89 throw new Error(result.message || "Payment not found");
90 }
91
92 const data = result.data;
93 const idNum = paymentNum(data.id);
94 const bookingId = paymentNum(data.booking_id);
95 const ref = paymentStr(data.booking_reference);
96 const row: ViewPaymentRow = {
97 id: idNum,
98 payment_number: `PAY-${String(idNum).padStart(6, "0")}`,
99 booking_id: bookingId,
100 booking_number: ref || `#${bookingId}`,
101 customer_name: paymentStr(data.customer_name, "N/A"),
102 customer_email: paymentStr(data.customer_email),
103 customer_phone: paymentStr(data.customer_phone),
104 trip_id: paymentNum(data.trip_id),
105 trip_title: paymentStr(data.trip_title),
106 amount: paymentNum(data.amount),
107 payment_method: paymentStr(data.gateway),
108 payment_status: paymentStr(data.status, "pending"),
109 transaction_id: paymentStr(data.transaction_id),
110 payment_date: paymentStr(data.processed_at || data.created_at),
111 notes: paymentStr(data.notes),
112 created_at: paymentStr(data.created_at),
113 updated_at: paymentStr(data.updated_at),
114 };
115 return row;
116 },
117 enabled: !!paymentId && can("yatra_view_bookings"),
118 });
119
120 const formatDate = (dateString: string) => {
121 return new Date(dateString).toLocaleDateString("en-US", {
122 weekday: "long",
123 year: "numeric",
124 month: "long",
125 day: "numeric",
126 });
127 };
128
129 const formatPrice = (price: number, currencyCode: string = "USD") => {
130 const symbol = getCurrencySymbol(currencyCode);
131 const currencyData = getCurrency(currencyCode);
132 const decimals = currencyData?.decimalDigits ?? 2;
133
134 return `${symbol}${new Intl.NumberFormat(undefined, {
135 minimumFractionDigits: decimals,
136 maximumFractionDigits: decimals,
137 }).format(price)}`;
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 if (isLoading) {
199 return (
200 <div className="space-y-3">
201 <PageHeader
202 title={__("Payment Details", "yatra")}
203 actions={
204 <Button
205 variant="outline"
206 onClick={handleBack}
207 className="flex items-center gap-2"
208 >
209 <ArrowLeft className="w-4 h-4" />
210 {__("Back", "yatra")}
211 </Button>
212 }
213 />
214 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
215 <div className="lg:col-span-2 space-y-3">
216 <Card>
217 <CardHeader className="pb-2">
218 <Skeleton className="h-6 w-40" />
219 </CardHeader>
220 <CardContent className="space-y-4">
221 <div className="grid grid-cols-2 gap-4">
222 <Skeleton className="h-12 w-full" />
223 <Skeleton className="h-12 w-full" />
224 <Skeleton className="h-12 w-full" />
225 <Skeleton className="h-12 w-full" />
226 </div>
227 </CardContent>
228 </Card>
229 <Card>
230 <CardHeader className="pb-2">
231 <Skeleton className="h-6 w-32" />
232 </CardHeader>
233 <CardContent>
234 <div className="grid grid-cols-2 gap-4">
235 <Skeleton className="h-12 w-full" />
236 <Skeleton className="h-12 w-full" />
237 </div>
238 </CardContent>
239 </Card>
240 </div>
241 <div className="space-y-3">
242 <Card>
243 <CardHeader className="pb-2">
244 <Skeleton className="h-6 w-24" />
245 </CardHeader>
246 <CardContent className="space-y-3">
247 <Skeleton className="h-5 w-full" />
248 <Skeleton className="h-5 w-3/4" />
249 <Skeleton className="h-5 w-1/2" />
250 </CardContent>
251 </Card>
252 </div>
253 </div>
254 </div>
255 );
256 }
257
258 if (error || !payment) {
259 return (
260 <div className="space-y-3">
261 <PageHeader
262 title={__("Payment Not Found", "yatra")}
263 actions={
264 <Button
265 variant="outline"
266 onClick={handleBack}
267 className="flex items-center gap-2"
268 >
269 <ArrowLeft className="w-4 h-4" />
270 {__("Back to Payments", "yatra")}
271 </Button>
272 }
273 />
274 <Card>
275 <CardContent className="p-8 text-center text-red-500">
276 {__(
277 "Payment not found or you do not have permission to view it.",
278 "yatra",
279 )}
280 </CardContent>
281 </Card>
282 </div>
283 );
284 }
285
286 return (
287 <div className="space-y-3">
288 <PageHeader
289 title={__("Payment Details", "yatra")}
290 description={__("View complete payment information", "yatra")}
291 actions={
292 <div className="flex items-center gap-2">
293 <Button
294 variant="outline"
295 onClick={handleBack}
296 className="flex items-center gap-2"
297 >
298 <ArrowLeft className="w-4 h-4" />
299 {__("Back", "yatra")}
300 </Button>
301 <ConditionalRender capability="yatra_edit_bookings">
302 <Button onClick={handleEdit} className="flex items-center gap-2">
303 <Edit className="w-4 h-4" />
304 {__("Edit Payment", "yatra")}
305 </Button>
306 </ConditionalRender>
307 </div>
308 }
309 />
310
311 <ConditionalRender capability="yatra_view_bookings">
312 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
313 {/* Main Content */}
314 <div className="lg:col-span-2 space-y-3">
315 {/* Payment Information */}
316 <Card>
317 <CardHeader className="pb-2">
318 <div className="flex items-center justify-between">
319 <CardTitle className="text-base">
320 {__("Payment Information", "yatra")}
321 </CardTitle>
322 {getPaymentStatusBadge(payment.payment_status)}
323 </div>
324 </CardHeader>
325 <CardContent className="space-y-4">
326 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
327 <div>
328 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
329 {__("Payment Number", "yatra")}
330 </label>
331 <p className="mt-1 text-sm font-mono font-semibold text-gray-900 dark:text-white">
332 {payment.payment_number}
333 </p>
334 </div>
335 <div>
336 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
337 {__("Payment Amount", "yatra")}
338 </label>
339 <p className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
340 {formatPrice(payment.amount)}
341 </p>
342 </div>
343 <div>
344 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
345 {__("Payment Method", "yatra")}
346 </label>
347 <div className="mt-1 flex items-center gap-2">
348 <CreditCard className="w-4 h-4 text-gray-400" />
349 <p className="text-sm font-medium text-gray-900 dark:text-white">
350 {payment.payment_method}
351 </p>
352 </div>
353 </div>
354 <div>
355 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
356 {__("Payment Date", "yatra")}
357 </label>
358 <div className="mt-1 flex items-center gap-2">
359 <Calendar className="w-4 h-4 text-gray-400" />
360 <p className="text-sm font-medium text-gray-900 dark:text-white">
361 {formatDate(payment.payment_date)}
362 </p>
363 </div>
364 </div>
365 {payment.transaction_id && (
366 <div>
367 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
368 {__("Transaction ID", "yatra")}
369 </label>
370 <p className="mt-1 text-sm font-mono text-gray-900 dark:text-white">
371 {payment.transaction_id}
372 </p>
373 </div>
374 )}
375 </div>
376
377 {payment.notes && (
378 <div className="pt-4 border-t border-gray-200 dark:border-gray-700">
379 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2 block">
380 {__("Notes", "yatra")}
381 </label>
382 <p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
383 {payment.notes}
384 </p>
385 </div>
386 )}
387 </CardContent>
388 </Card>
389
390 {/* Booking Information */}
391 <Card>
392 <CardHeader className="pb-2">
393 <div className="flex items-center justify-between">
394 <CardTitle className="text-base">
395 {__("Related Booking", "yatra")}
396 </CardTitle>
397 <Button
398 variant="ghost"
399 size="sm"
400 onClick={handleViewBooking}
401 className="flex items-center gap-1.5 text-sm"
402 >
403 {__("View Booking", "yatra")}
404 <ExternalLink className="w-3.5 h-3.5" />
405 </Button>
406 </div>
407 </CardHeader>
408 <CardContent className="space-y-4">
409 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
410 <div>
411 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
412 {__("Booking Number", "yatra")}
413 </label>
414 <p className="mt-1 text-sm font-mono font-semibold text-gray-900 dark:text-white">
415 {payment.booking_number}
416 </p>
417 </div>
418 <div>
419 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
420 {__("Trip", "yatra")}
421 </label>
422 <p className="mt-1 text-sm font-medium text-gray-900 dark:text-white">
423 {payment.trip_title}
424 </p>
425 </div>
426 </div>
427 </CardContent>
428 </Card>
429 </div>
430
431 {/* Sidebar */}
432 <div className="space-y-3">
433 {/* Customer Information */}
434 <Card>
435 <CardHeader className="pb-2">
436 <CardTitle className="text-base">
437 {__("Customer", "yatra")}
438 </CardTitle>
439 </CardHeader>
440 <CardContent className="space-y-3">
441 <div>
442 <p className="text-sm font-medium text-gray-900 dark:text-white">
443 {payment.customer_name}
444 </p>
445 </div>
446 <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
447 <Mail className="w-4 h-4" />
448 <span>{payment.customer_email}</span>
449 </div>
450 {payment.customer_phone && (
451 <div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
452 <Phone className="w-4 h-4" />
453 <span>{payment.customer_phone}</span>
454 </div>
455 )}
456 </CardContent>
457 </Card>
458
459 {/* Payment Timeline */}
460 <Card>
461 <CardHeader className="pb-2">
462 <CardTitle className="text-base">
463 {__("Timeline", "yatra")}
464 </CardTitle>
465 </CardHeader>
466 <CardContent className="space-y-3">
467 <div>
468 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
469 {__("Created", "yatra")}
470 </label>
471 <p className="mt-1 text-sm text-gray-900 dark:text-white">
472 {formatDate(payment.created_at)}
473 </p>
474 </div>
475 {payment.updated_at &&
476 payment.updated_at !== payment.created_at && (
477 <div>
478 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
479 {__("Last Updated", "yatra")}
480 </label>
481 <p className="mt-1 text-sm text-gray-900 dark:text-white">
482 {formatDate(payment.updated_at)}
483 </p>
484 </div>
485 )}
486 </CardContent>
487 </Card>
488 </div>
489 </div>
490 </ConditionalRender>
491 </div>
492 );
493 };
494
495 export default ViewPayment;
496