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

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

944 lines 29.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Payments Page
3 * Manage payments for bookings
4 */
5
6 import React, { useState, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import {
9 Plus,
10 ArrowUpDown,
11 ArrowUp,
12 ArrowDown,
13 Edit,
14 Trash2,
15 Eye,
16 CreditCard,
17 } from "lucide-react";
18 import {
19 Pagination,
20 SearchFilterToolbar,
21 BulkActionToolbar,
22 Table as SharedTable,
23 } from "../components/shared";
24 import { __ } from "../lib/i18n";
25 import { getErrorContext } from "../lib/errors";
26 import { apiService } from "../lib/api-client";
27 import { usePermissions } from "../hooks/usePermissions";
28 import { useToast } from "../components/ui/toast";
29 import { Button } from "../components/ui/button";
30 import { Select } from "../components/ui/select";
31 import { PageHeader } from "../components/common/PageHeader";
32 import { Card, CardContent } from "../components/ui/card";
33 import { ConditionalRender } from "../components/ui/conditional-render";
34 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
35 import { getCurrencySymbol, getCurrency } from "../data/currencies";
36 import { formatDate as formatDateUtil } from "../lib/dateFormat";
37
38 interface Payment {
39 id: number;
40 payment_number: string;
41 booking_id: number;
42 booking_number: string;
43 customer_name: string;
44 customer_email: string;
45 trip_title: string;
46 amount: number;
47 currency?: string;
48 payment_method: string;
49 payment_status:
50 | "pending"
51 | "completed"
52 | "failed"
53 | "refunded"
54 | "partial"
55 | "cancelled";
56 transaction_id?: string;
57 payment_date: string;
58 notes?: string;
59 created_at: string;
60 }
61
62 const Payments: React.FC = () => {
63 const [searchTerm, setSearchTerm] = useState("");
64 const [statusFilter, setStatusFilter] = useState("all");
65 const [methodFilter, setMethodFilter] = useState("all");
66 const [sortBy, setSortBy] = useState("payment_date");
67 const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
68 const [page, setPage] = useState(1);
69 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
70 const [bulkAction, setBulkAction] = useState("");
71 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
72 const [isBulkPending, setIsBulkPending] = useState(false);
73 const [deleteConfirm, setDeleteConfirm] = useState<{
74 isOpen: boolean;
75 payment: Payment | null;
76 }>({
77 isOpen: false,
78 payment: null,
79 });
80 const [visibleColumns, setVisibleColumns] = useState(() => {
81 if (typeof window === "undefined") {
82 return {
83 payment: true,
84 customer: true,
85 booking: true,
86 amount: true,
87 method: true,
88 status: true,
89 date: true,
90 };
91 }
92 const saved = window.localStorage.getItem("yatra-payments-visible-columns");
93 return saved
94 ? JSON.parse(saved)
95 : {
96 payment: true,
97 customer: true,
98 booking: true,
99 amount: true,
100 method: true,
101 status: true,
102 date: true,
103 };
104 });
105 const queryClient = useQueryClient();
106 const { can } = usePermissions();
107 const { showToast } = useToast();
108 const defaultCurrency =
109 (window as any)?.yatraAdmin?.currency ||
110 (window as any)?.yatraBookingData?.currency ||
111 "USD";
112
113 // Fetch payments from API
114
115 const PAYMENTS_PER_PAGE = 10;
116
117 const queryParams = useMemo(() => {
118 const params: Record<string, any> = {
119 page,
120 per_page: PAYMENTS_PER_PAGE,
121 orderby: sortBy,
122 order: sortOrder,
123 };
124
125 if (searchTerm) {
126 params.search = searchTerm;
127 }
128
129 if (statusFilter !== "all") {
130 params.payment_status = statusFilter;
131 }
132
133 if (methodFilter !== "all") {
134 params.payment_method = methodFilter;
135 }
136
137 return params;
138 }, [searchTerm, statusFilter, methodFilter, sortBy, sortOrder, page]);
139
140 // Fetch payments from API
141 const { data, isLoading, error } = useQuery({
142 queryKey: ["payments", queryParams],
143 queryFn: async () => {
144 const paramsObj: Record<string, any> = {
145 page: queryParams.page,
146 per_page: queryParams.per_page,
147 };
148 if (queryParams.search) {
149 paramsObj.search = queryParams.search;
150 }
151 if (queryParams.payment_status) {
152 paramsObj.status = queryParams.payment_status;
153 }
154 if (queryParams.payment_method) {
155 paramsObj.gateway = queryParams.payment_method;
156 }
157
158 return await apiService.getPayments(paramsObj);
159 },
160 enabled: can("yatra_view_bookings"),
161 });
162
163 const { data: paymentStatsRaw } = useQuery({
164 queryKey: ["payments-stats"],
165 queryFn: () => apiService.getPaymentsStats(),
166 enabled: can("yatra_view_bookings"),
167 });
168
169 // Delete mutation
170 const deleteMutation = useMutation({
171 mutationFn: async (id: number) => {
172 await apiService.deletePayment(id);
173 },
174 onSuccess: () => {
175 queryClient.invalidateQueries({ queryKey: ["payments"] });
176 queryClient.invalidateQueries({ queryKey: ["payments-stats"] });
177 queryClient.invalidateQueries({ queryKey: ["bookings"] });
178 showToast(__("Payment deleted successfully", "yatra"), "success");
179 setDeleteConfirm({ isOpen: false, payment: null });
180 },
181 onError: (error: any) => {
182 showToast(
183 error?.message || __("Failed to delete payment", "yatra"),
184 "error",
185 );
186 },
187 });
188
189 const payments: Payment[] = data?.data || [];
190 const listMeta = (data as any)?.meta;
191 const total = Number(listMeta?.total ?? (data as any)?.total ?? 0);
192 const perPage =
193 Number(
194 listMeta?.per_page ?? (data as any)?.per_page ?? PAYMENTS_PER_PAGE,
195 ) || PAYMENTS_PER_PAGE;
196 const totalPages = Math.max(
197 1,
198 Number(listMeta?.total_pages ?? (data as any)?.total_pages) ||
199 (total > 0 ? Math.ceil(total / perPage) : 1),
200 );
201
202 const payCounts = (paymentStatsRaw ?? {}) as Record<string, number>;
203 const countFor = (key: string) => Number(payCounts[key] ?? 0);
204 const errorContext = getErrorContext(error);
205
206 const formatDate = (dateString: string) => {
207 return formatDateUtil(dateString);
208 };
209
210 const formatPrice = (
211 price: number,
212 currencyCode: string = defaultCurrency,
213 ) => {
214 const symbol = getCurrencySymbol(currencyCode);
215 const currencyData = getCurrency(currencyCode);
216 const decimals = currencyData?.decimalDigits ?? 2;
217
218 return `${symbol}${new Intl.NumberFormat(undefined, {
219 minimumFractionDigits: decimals,
220 maximumFractionDigits: decimals,
221 }).format(price)}`;
222 };
223
224 const getStatusBadge = (status: string) => {
225 const statusMap: Record<string, { className: string; label: string }> = {
226 completed: {
227 className:
228 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
229 label: __("Completed", "yatra"),
230 },
231 pending: {
232 className:
233 "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400",
234 label: __("Pending", "yatra"),
235 },
236 partial: {
237 className:
238 "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
239 label: __("Partial", "yatra"),
240 },
241 failed: {
242 className:
243 "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400",
244 label: __("Failed", "yatra"),
245 },
246 refunded: {
247 className:
248 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
249 label: __("Refunded", "yatra"),
250 },
251 cancelled: {
252 className:
253 "bg-orange-100 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400",
254 label: __("Cancelled", "yatra"),
255 },
256 };
257
258 const statusInfo = statusMap[status] || {
259 className:
260 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
261 label: status,
262 };
263
264 return (
265 <span
266 className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${statusInfo.className}`}
267 >
268 {statusInfo.label}
269 </span>
270 );
271 };
272
273 const handleEdit = (payment: Payment) => {
274 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=payments&action=edit&id=${payment.id}`;
275 };
276
277 const handleDelete = (payment: Payment) => {
278 setDeleteConfirm({ isOpen: true, payment });
279 };
280
281 const confirmDelete = () => {
282 if (deleteConfirm.payment) {
283 deleteMutation.mutate(deleteConfirm.payment.id);
284 }
285 };
286
287 const handleView = (payment: Payment) => {
288 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=payments&action=view&id=${payment.id}`;
289 };
290
291 const handleCreatePayment = () => {
292 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=payments&action=create`;
293 };
294
295 const handleResetFilters = () => {
296 setSearchTerm("");
297 setStatusFilter("all");
298 setMethodFilter("all");
299 setSortBy("payment_date");
300 setSortOrder("desc");
301 setPage(1);
302 };
303
304 const handleSort = (field: string) => {
305 if (sortBy === field) {
306 setSortOrder(sortOrder === "asc" ? "desc" : "asc");
307 } else {
308 setSortBy(field);
309 setSortOrder("desc");
310 }
311 };
312
313 const getSortIcon = (field: string) => {
314 if (sortBy !== field) {
315 return <ArrowUpDown className="w-3.5 h-3.5 ml-1 text-gray-400" />;
316 }
317 return sortOrder === "asc" ? (
318 <ArrowUp className="w-3.5 h-3.5 ml-1 text-gray-600 dark:text-gray-300" />
319 ) : (
320 <ArrowDown className="w-3.5 h-3.5 ml-1 text-gray-600 dark:text-gray-300" />
321 );
322 };
323
324 const hasFilters =
325 searchTerm ||
326 statusFilter !== "all" ||
327 methodFilter !== "all" ||
328 sortBy !== "payment_date" ||
329 sortOrder !== "desc";
330
331 const toggleColumn = (columnKey: string) => {
332 const newVisibleColumns = {
333 ...visibleColumns,
334 [columnKey]: !visibleColumns[columnKey as keyof typeof visibleColumns],
335 };
336 setVisibleColumns(newVisibleColumns);
337 if (typeof window !== "undefined") {
338 window.localStorage.setItem(
339 "yatra-payments-visible-columns",
340 JSON.stringify(newVisibleColumns),
341 );
342 }
343 };
344
345 const handleSelectItem = (id: string | number, checked: boolean) => {
346 setSelectedIds((prev) =>
347 checked ? [...prev, id] : prev.filter((selectedId) => selectedId !== id),
348 );
349 };
350
351 const handleSelectAll = (checked: boolean) => {
352 if (checked) {
353 setSelectedIds(payments.map((payment: Payment) => payment.id));
354 } else {
355 setSelectedIds([]);
356 }
357 };
358
359 const isAllSelected =
360 payments.length > 0 && selectedIds.length === payments.length;
361
362 const updateStatusForIds = async (
363 ids: (string | number)[],
364 newStatus: Payment["payment_status"],
365 ) => {
366 await Promise.all(
367 ids.map(async (id) => {
368 await apiService.updatePaymentStatus(id, newStatus);
369 }),
370 );
371 };
372
373 const handleBulkApply = async () => {
374 if (!bulkAction || selectedIds.length === 0) {
375 return;
376 }
377
378 try {
379 setIsBulkPending(true);
380
381 if (bulkAction === "delete") {
382 await apiService.bulkPaymentsAction("delete", selectedIds);
383 showToast(
384 __("Selected payments deleted successfully", "yatra"),
385 "success",
386 );
387 } else {
388 const newStatus = bulkAction as Payment["payment_status"];
389 await updateStatusForIds(selectedIds, newStatus);
390 showToast(__("Bulk status updated successfully", "yatra"), "success");
391 }
392
393 queryClient.invalidateQueries({ queryKey: ["payments"] });
394 queryClient.invalidateQueries({ queryKey: ["payments-stats"] });
395 queryClient.invalidateQueries({ queryKey: ["bookings"] });
396 setSelectedIds([]);
397 setBulkAction("");
398 } catch (error: any) {
399 showToast(
400 error?.message ||
401 __("Failed to perform bulk action on payments", "yatra"),
402 "error",
403 );
404 } finally {
405 setIsBulkPending(false);
406 }
407 };
408
409 const allBulkActionOptions = [
410 { value: "completed", label: __("Mark as Completed", "yatra") },
411 { value: "failed", label: __("Mark as Failed", "yatra") },
412 { value: "refunded", label: __("Mark as Refunded", "yatra") },
413 { value: "cancelled", label: __("Mark as Cancelled", "yatra") },
414 { value: "delete", label: __("Delete permanently", "yatra") },
415 ];
416
417 const getBulkActionOptionsForStatus = (view: string) => {
418 switch (view) {
419 case "completed":
420 return allBulkActionOptions.filter((opt) =>
421 ["failed", "refunded", "cancelled", "delete"].includes(opt.value),
422 );
423 case "pending":
424 return allBulkActionOptions.filter((opt) =>
425 ["completed", "failed", "cancelled", "delete"].includes(opt.value),
426 );
427 case "partial":
428 return allBulkActionOptions.filter((opt) =>
429 ["completed", "cancelled", "delete"].includes(opt.value),
430 );
431 case "failed":
432 return allBulkActionOptions.filter((opt) =>
433 ["completed", "cancelled", "delete"].includes(opt.value),
434 );
435 case "refunded":
436 return allBulkActionOptions.filter((opt) =>
437 ["completed", "cancelled", "delete"].includes(opt.value),
438 );
439 case "cancelled":
440 return allBulkActionOptions.filter((opt) =>
441 ["completed", "delete"].includes(opt.value),
442 );
443 default:
444 return allBulkActionOptions;
445 }
446 };
447
448 const bulkActionOptions = getBulkActionOptionsForStatus(statusFilter);
449
450 const statusOptions = [
451 { value: "all", label: __("All Status", "yatra") },
452 { value: "completed", label: __("Completed", "yatra") },
453 { value: "pending", label: __("Pending", "yatra") },
454 { value: "partial", label: __("Partial", "yatra") },
455 { value: "failed", label: __("Failed", "yatra") },
456 { value: "refunded", label: __("Refunded", "yatra") },
457 { value: "cancelled", label: __("Cancelled", "yatra") },
458 ];
459
460 const sortOptions = [
461 { value: "payment_date", label: __("Payment Date", "yatra") },
462 { value: "payment_number", label: __("Payment Number", "yatra") },
463 { value: "customer_name", label: __("Customer", "yatra") },
464 { value: "amount", label: __("Amount", "yatra") },
465 { value: "payment_method", label: __("Payment Method", "yatra") },
466 { value: "payment_status", label: __("Status", "yatra") },
467 ];
468
469 const columnOptions = [
470 {
471 key: "payment",
472 label: __("Payment", "yatra"),
473 visible: visibleColumns.payment,
474 },
475 {
476 key: "customer",
477 label: __("Customer", "yatra"),
478 visible: visibleColumns.customer,
479 },
480 {
481 key: "booking",
482 label: __("Booking", "yatra"),
483 visible: visibleColumns.booking,
484 },
485 {
486 key: "amount",
487 label: __("Amount", "yatra"),
488 visible: visibleColumns.amount,
489 },
490 {
491 key: "method",
492 label: __("Method", "yatra"),
493 visible: visibleColumns.method,
494 },
495 {
496 key: "status",
497 label: __("Status", "yatra"),
498 visible: visibleColumns.status,
499 },
500 { key: "date", label: __("Date", "yatra"), visible: visibleColumns.date },
501 ];
502
503 const columns = [
504 {
505 key: "payment",
506 label: __("Payment", "yatra"),
507 sortable: true,
508 visible: visibleColumns.payment,
509 width: "w-[220px]",
510 render: (payment: Payment) => (
511 <div>
512 <button
513 type="button"
514 onClick={() => handleView(payment)}
515 className="flex items-center gap-2 mb-1 group cursor-pointer"
516 >
517 <CreditCard className="w-4 h-4 text-blue-500 dark:text-blue-400 group-hover:text-blue-600 dark:group-hover:text-blue-300" />
518 <span className="font-medium text-blue-600 dark:text-blue-400 font-mono text-sm group-hover:text-blue-700 dark:group-hover:text-blue-300 group-hover:underline">
519 {payment.payment_number}
520 </span>
521 </button>
522 {payment.transaction_id && (
523 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
524 {__("TXN", "yatra")}: {payment.transaction_id}
525 </div>
526 )}
527 </div>
528 ),
529 },
530 {
531 key: "customer",
532 label: __("Customer", "yatra"),
533 sortable: true,
534 visible: visibleColumns.customer,
535 render: (payment: Payment) => (
536 <div>
537 <div className="text-sm font-medium text-gray-900 dark:text-white">
538 {payment.customer_name}
539 </div>
540 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
541 {payment.customer_email}
542 </div>
543 </div>
544 ),
545 },
546 {
547 key: "booking",
548 label: __("Booking", "yatra"),
549 sortable: false,
550 visible: visibleColumns.booking,
551 render: (payment: Payment) => (
552 <div>
553 <div className="text-sm font-medium text-gray-900 dark:text-white">
554 {payment.booking_number}
555 </div>
556 <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
557 {payment.trip_title}
558 </div>
559 </div>
560 ),
561 },
562 {
563 key: "amount",
564 label: __("Amount", "yatra"),
565 sortable: true,
566 visible: visibleColumns.amount,
567 render: (payment: Payment) => (
568 <span className="font-semibold text-gray-900 dark:text-white">
569 {formatPrice(payment.amount, payment.currency || defaultCurrency)}
570 </span>
571 ),
572 },
573 {
574 key: "method",
575 label: __("Method", "yatra"),
576 sortable: true,
577 visible: visibleColumns.method,
578 render: (payment: Payment) => (
579 <span className="text-gray-600 dark:text-gray-400">
580 {payment.payment_method}
581 </span>
582 ),
583 },
584 {
585 key: "status",
586 label: __("Status", "yatra"),
587 sortable: true,
588 visible: visibleColumns.status,
589 render: (payment: Payment) => getStatusBadge(payment.payment_status),
590 },
591 {
592 key: "date",
593 label: __("Date", "yatra"),
594 sortable: true,
595 visible: visibleColumns.date,
596 render: (payment: Payment) => (
597 <span className="text-gray-500 dark:text-gray-400 text-sm">
598 {formatDate(payment.payment_date)}
599 </span>
600 ),
601 },
602 ];
603
604 const actions = [
605 {
606 key: "view",
607 label: __("View", "yatra"),
608 icon: <Eye className="w-4 h-4" />,
609 onClick: (payment: Payment) => handleView(payment),
610 },
611 {
612 key: "edit",
613 label: __("Edit", "yatra"),
614 icon: <Edit className="w-4 h-4" />,
615 onClick: (payment: Payment) => handleEdit(payment),
616 },
617 {
618 key: "mark_completed",
619 label: __("Mark as Completed", "yatra"),
620 icon: <ArrowUp className="w-4 h-4" />,
621 onClick: async (payment: Payment) => {
622 setIsBulkPending(true);
623 try {
624 await updateStatusForIds([payment.id], "completed");
625 queryClient.invalidateQueries({ queryKey: ["payments"] });
626 queryClient.invalidateQueries({ queryKey: ["payments-stats"] });
627 queryClient.invalidateQueries({ queryKey: ["bookings"] });
628 showToast(__("Payment status updated", "yatra"), "success");
629 } catch (error: any) {
630 showToast(
631 error?.message || __("Failed to update payment status", "yatra"),
632 "error",
633 );
634 } finally {
635 setIsBulkPending(false);
636 }
637 },
638 condition: (payment: Payment) =>
639 can("yatra_edit_bookings") && payment.payment_status !== "completed",
640 },
641 {
642 key: "mark_failed",
643 label: __("Mark as Failed", "yatra"),
644 icon: <ArrowDown className="w-4 h-4" />,
645 onClick: async (payment: Payment) => {
646 setIsBulkPending(true);
647 try {
648 await updateStatusForIds([payment.id], "failed");
649 queryClient.invalidateQueries({ queryKey: ["payments"] });
650 queryClient.invalidateQueries({ queryKey: ["payments-stats"] });
651 queryClient.invalidateQueries({ queryKey: ["bookings"] });
652 showToast(__("Payment status updated", "yatra"), "success");
653 } catch (error: any) {
654 showToast(
655 error?.message || __("Failed to update payment status", "yatra"),
656 "error",
657 );
658 } finally {
659 setIsBulkPending(false);
660 }
661 },
662 condition: (payment: Payment) =>
663 can("yatra_edit_bookings") && payment.payment_status !== "failed",
664 },
665 {
666 key: "mark_refunded",
667 label: __("Mark as Refunded", "yatra"),
668 icon: <ArrowDown className="w-4 h-4" />,
669 onClick: async (payment: Payment) => {
670 setIsBulkPending(true);
671 try {
672 await updateStatusForIds([payment.id], "refunded");
673 queryClient.invalidateQueries({ queryKey: ["payments"] });
674 queryClient.invalidateQueries({ queryKey: ["payments-stats"] });
675 queryClient.invalidateQueries({ queryKey: ["bookings"] });
676 showToast(__("Payment status updated", "yatra"), "success");
677 } catch (error: any) {
678 showToast(
679 error?.message || __("Failed to update payment status", "yatra"),
680 "error",
681 );
682 } finally {
683 setIsBulkPending(false);
684 }
685 },
686 condition: (payment: Payment) =>
687 can("yatra_edit_bookings") && payment.payment_status !== "refunded",
688 },
689 {
690 key: "mark_cancelled",
691 label: __("Mark as Cancelled", "yatra"),
692 icon: <ArrowDown className="w-4 h-4" />,
693 onClick: async (payment: Payment) => {
694 setIsBulkPending(true);
695 try {
696 await updateStatusForIds([payment.id], "cancelled");
697 queryClient.invalidateQueries({ queryKey: ["payments"] });
698 queryClient.invalidateQueries({ queryKey: ["payments-stats"] });
699 queryClient.invalidateQueries({ queryKey: ["bookings"] });
700 showToast(__("Payment status updated", "yatra"), "success");
701 } catch (error: any) {
702 showToast(
703 error?.message || __("Failed to update payment status", "yatra"),
704 "error",
705 );
706 } finally {
707 setIsBulkPending(false);
708 }
709 },
710 condition: (payment: Payment) =>
711 can("yatra_edit_bookings") && payment.payment_status !== "cancelled",
712 },
713 {
714 key: "delete",
715 label: __("Delete", "yatra"),
716 icon: <Trash2 className="w-4 h-4" />,
717 onClick: (payment: Payment) => handleDelete(payment),
718 variant: "destructive" as const,
719 },
720 ];
721
722 return (
723 <div className="space-y-3">
724 <ConfirmationDialog
725 isOpen={deleteConfirm.isOpen}
726 onClose={() => setDeleteConfirm({ isOpen: false, payment: null })}
727 onConfirm={confirmDelete}
728 title={__("Delete Payment", "yatra")}
729 message={
730 deleteConfirm.payment
731 ? __(
732 'Are you sure you want to delete payment "{number}"? This action cannot be undone.',
733 "yatra",
734 ).replace("{number}", deleteConfirm.payment.payment_number)
735 : __(
736 "Are you sure you want to delete this payment? This action cannot be undone.",
737 "yatra",
738 )
739 }
740 confirmText={__("Delete", "yatra")}
741 cancelText={__("Cancel", "yatra")}
742 variant="danger"
743 isLoading={deleteMutation.isPending}
744 />
745
746 <PageHeader
747 title={__("Payments", "yatra")}
748 description={__("Manage payment records for bookings", "yatra")}
749 actionCapability="yatra_edit_bookings"
750 actions={
751 <Button
752 onClick={handleCreatePayment}
753 className="flex items-center gap-2"
754 >
755 <Plus className="w-4 h-4" />
756 {__("Add New Payment", "yatra")}
757 </Button>
758 }
759 />
760
761 <Card>
762 <CardContent className="p-3">
763 <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2">
764 <div className="flex-1 min-w-0">
765 <SearchFilterToolbar
766 searchTerm={searchTerm}
767 onSearchChange={setSearchTerm}
768 statusFilter={statusFilter}
769 onStatusChange={(value) => {
770 setStatusFilter(value);
771 setPage(1);
772 }}
773 statusOptions={statusOptions}
774 sortBy={sortBy}
775 onSortByChange={(field) => {
776 setSortBy(field);
777 setPage(1);
778 }}
779 sortOrder={sortOrder}
780 onSortOrderChange={(order) => {
781 setSortOrder(order);
782 setPage(1);
783 }}
784 sortOptions={sortOptions}
785 onResetFilters={handleResetFilters}
786 hasFilters={!!hasFilters}
787 placeholder={__("Search payments...", "yatra")}
788 />
789 </div>
790
791 <div className="w-full lg:w-auto flex lg:justify-end">
792 <Select
793 value={methodFilter}
794 onChange={(e) => {
795 setMethodFilter(e.target.value);
796 setPage(1);
797 }}
798 className="w-full lg:w-48 max-w-xs"
799 >
800 <option value="all">{__("All Methods", "yatra")}</option>
801 <option value="Credit Card">
802 {__("Credit Card", "yatra")}
803 </option>
804 <option value="PayPal">{__("PayPal", "yatra")}</option>
805 <option value="Bank Transfer">
806 {__("Bank Transfer", "yatra")}
807 </option>
808 <option value="Cash">{__("Cash", "yatra")}</option>
809 </Select>
810 </div>
811 </div>
812 </CardContent>
813 </Card>
814
815 <ConditionalRender capability="yatra_view_bookings">
816 <>
817 {!error && (
818 <BulkActionToolbar
819 selectedIds={selectedIds}
820 bulkAction={bulkAction}
821 setBulkAction={setBulkAction}
822 onApply={handleBulkApply}
823 onClearSelection={() => setSelectedIds([])}
824 statusFilter={statusFilter}
825 setStatusFilter={(value) => {
826 setStatusFilter(value);
827 setPage(1);
828 }}
829 statusOptions={[
830 {
831 key: "all",
832 label: __("All", "yatra"),
833 count: countFor("all"),
834 },
835 {
836 key: "completed",
837 label: __("Completed", "yatra"),
838 count: countFor("completed"),
839 },
840 {
841 key: "pending",
842 label: __("Pending", "yatra"),
843 count: countFor("pending"),
844 },
845 {
846 key: "partial",
847 label: __("Partial", "yatra"),
848 count: countFor("partial"),
849 },
850 {
851 key: "failed",
852 label: __("Failed", "yatra"),
853 count: countFor("failed"),
854 },
855 {
856 key: "refunded",
857 label: __("Refunded", "yatra"),
858 count: countFor("refunded"),
859 },
860 {
861 key: "cancelled",
862 label: __("Cancelled", "yatra"),
863 count: countFor("cancelled"),
864 },
865 ]}
866 showColumnsDropdown={showColumnsDropdown}
867 setShowColumnsDropdown={setShowColumnsDropdown}
868 columnOptions={columnOptions}
869 onToggleColumn={toggleColumn}
870 bulkMutationPending={isBulkPending}
871 totalItems={total}
872 bulkActionOptions={bulkActionOptions}
873 />
874 )}
875
876 <Card>
877 <CardContent className="p-0">
878 <SharedTable
879 data={payments}
880 columns={columns}
881 actions={actions}
882 isLoading={isLoading}
883 isError={!!error}
884 errorText={__("Error loading payments", "yatra")}
885 errorDescription={__(
886 "We couldn’t connect to the payments service. Please refresh or try again in a moment.",
887 "We couldn’t connect to the payments service. Please refresh or try again in a moment.",
888 )}
889 onRetry={() => {
890 queryClient.invalidateQueries({ queryKey: ["payments"] });
891 queryClient.invalidateQueries({
892 queryKey: ["payments-stats"],
893 });
894 }}
895 errorDetails={errorContext.details}
896 errorRequestInfo={errorContext.requestInfo}
897 emptyText={__("No payments found", "yatra")}
898 emptyDescription={
899 hasFilters
900 ? __(
901 "Try adjusting your filters to see more results.",
902 "yatra",
903 )
904 : __(
905 "Get started by recording your first payment.",
906 "yatra",
907 )
908 }
909 onCreateClick={
910 can("yatra_edit_bookings") ? handleCreatePayment : undefined
911 }
912 onSort={handleSort}
913 getSortIcon={getSortIcon}
914 selectedItemIds={selectedIds}
915 onSelectItem={handleSelectItem}
916 onSelectAll={handleSelectAll}
917 isAllSelected={isAllSelected}
918 getItemId={(payment: Payment) => payment.id}
919 skeletonRows={5}
920 capability="yatra_view_bookings"
921 />
922 </CardContent>
923 </Card>
924
925 {!error && total > 0 && (
926 <div className="mt-4">
927 <Pagination
928 currentPage={page}
929 totalPages={totalPages}
930 totalItems={total}
931 itemsPerPage={perPage}
932 onPageChange={(newPage) => setPage(newPage)}
933 itemName={__("payments", "yatra")}
934 />
935 </div>
936 )}
937 </>
938 </ConditionalRender>
939 </div>
940 );
941 };
942
943 export default Payments;
944