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

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