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

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

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