PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
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.9, at resources/js/pages/Payments.tsx

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