PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | resources/js/pages/Bookings.tsx +107 -15 3.0.13 → 3.0.17 View file →
@@ -16,8 +16,10 @@
16 16 AlertTriangle,
17 17 CreditCard,
18 18 CircleDollarSign,
19 19 Undo2,
20 + Mail,
21 + Download,
20 22 } from "lucide-react";
21 23 import {
22 24 Pagination,
23 25 SearchFilterToolbar,
@@ -24,8 +26,13 @@
24 26 BulkActionToolbar,
25 27 Table as SharedTable,
26 28 } from "../components/shared";
27 29 import { __ } from "../lib/i18n";
30 +import { downloadAdminBookingInvoice } from "../lib/invoice-download";
31 +import {
32 + buildYatraSinglePublicUrls,
33 + isWordPressPlainPermalink,
34 +} from "../lib/frontend-permalink-urls";
28 35 import { useToast } from "../components/ui/toast";
29 36 import { Button } from "../components/ui/button";
30 37 import { Select } from "../components/ui/select";
31 38 import { PageHeader } from "../components/common/PageHeader";
@@ -32,9 +39,9 @@
32 39 import { Card, CardContent } from "../components/ui/card";
33 40 import { ConditionalRender } from "../components/ui/conditional-render";
34 41 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
35 42 import { formatYatraMoney } from "../lib/currency-display";
36 -import { apiService } from "../lib/api-client";
43 +import { apiService, apiClient } from "../lib/api-client";
37 44 import { formatDate as formatDateUtil } from "../lib/dateFormat";
38 45 import { getErrorContext } from "../lib/errors";
39 46 import { usePermissions } from "../hooks/usePermissions";
40 47
@@ -44,8 +51,9 @@
44 51 customer_name: string;
45 52 customer_email: string;
46 53 trip_title: string;
47 54 trip_id: number;
55 + trip_slug?: string;
48 56 booking_date: string;
49 57 travel_date: string;
50 58 travelers?: number;
51 59 travelers_count?: number;
@@ -58,9 +66,8 @@
58 66 payment_gateway?: string;
59 67 created_at: string;
60 68 }
61 69
62 -
63 70 /**
64 71 * Payment statuses an administrator can set straight from the booking list.
65 72 *
66 73 * Labels are prefixed with "Payment:" because the same menu already carries
@@ -212,8 +219,11 @@
212 219 per_page: queryParams.per_page,
213 220 search: queryParams.search,
214 221 status: queryParams.status,
215 222 payment_status: queryParams.payment_status,
223 + // Forward sort params so column-header sorting reaches the backend.
224 + orderby: queryParams.orderby,
225 + order: queryParams.order,
216 226 });
217 227 },
218 228 enabled: canViewBookings,
219 229 });
@@ -658,21 +668,45 @@
658 668 key: "trip",
659 669 label: __("Trip"),
660 670 sortable: true,
661 671 visible: visibleColumns.trip,
662 - render: (booking: Booking) => (
663 - <div className="flex flex-col">
664 - <a
665 - href={`${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&action=edit&id=${booking.trip_id}`}
666 - className="font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
667 - >
668 - {booking.trip_title}
669 - </a>
670 - <span className="ml-1 text-[11px] text-gray-400 dark:text-gray-500">
671 - {booking.trip_id ? `(ID: ${booking.trip_id})` : ""}
672 - </span>
673 - </div>
674 - ),
672 + render: (booking: Booking) => {
673 + // Link to the tour's PUBLIC single page (opens in a new tab), not the
674 + // tour editor — a booking overview should never be a one-click path to
675 + // editing the underlying tour. Without a slug we can't build the public
676 + // URL, so the name is shown as plain text rather than falling back to
677 + // the editor.
678 + const slug = booking.trip_slug || "";
679 + let tourUrl = "";
680 + if (slug) {
681 + const { plainUrl, prettyUrl } = buildYatraSinglePublicUrls({
682 + entity: "trip",
683 + slug,
684 + });
685 + tourUrl = isWordPressPlainPermalink() ? plainUrl : prettyUrl;
686 + }
687 + return (
688 + <div className="flex flex-col">
689 + {tourUrl ? (
690 + <a
691 + href={tourUrl}
692 + target="_blank"
693 + rel="noopener noreferrer"
694 + className="font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
695 + >
696 + {booking.trip_title}
697 + </a>
698 + ) : (
699 + <span className="font-medium text-gray-900 dark:text-white">
700 + {booking.trip_title}
701 + </span>
702 + )}
703 + <span className="ml-1 text-[11px] text-gray-400 dark:text-gray-500">
704 + {booking.trip_id ? `(ID: ${booking.trip_id})` : ""}
705 + </span>
706 + </div>
707 + );
708 + },
675 709 },
676 710 {
677 711 key: "travelers",
678 712 label: __("Travelers"),
@@ -738,8 +772,27 @@
738 772 getBookingStatusBadge(booking.booking_status),
739 773 },
740 774 ];
741 775
776 + /**
777 + * Manually (re)send a transactional email for a booking from the row actions.
778 + * Errors and disabled-recipient cases surface via the endpoint's message.
779 + */
780 + const resendBookingEmail = async (bookingId: number, type: string) => {
781 + try {
782 + const res: any = await apiClient.post(
783 + `/bookings/${bookingId}/send-email`,
784 + { type },
785 + );
786 + if (res?.success === false) {
787 + throw new Error(res.message || __("Failed to send email"));
788 + }
789 + showToast(res?.message || __("Email sent"), "success");
790 + } catch (error: any) {
791 + showToast(error?.message || __("Failed to send email"), "error");
792 + }
793 + };
794 +
742 795 const actions = [
743 796 {
744 797 key: "view",
745 798 label: __("View"),
@@ -855,8 +908,47 @@
855 908 // the booking-status actions above behave.
856 909 condition: (booking: Booking) =>
857 910 can("yatra_edit_bookings") && booking.payment_status !== action.value,
858 911 })),
912 + {
913 + key: "download_invoice",
914 + label: __("Download invoice"),
915 + icon: <Download className="w-4 h-4" />,
916 + onClick: async (booking: Booking) => {
917 + try {
918 + await downloadAdminBookingInvoice(booking.id);
919 + } catch (error: any) {
920 + showToast(
921 + error?.message || __("Failed to download invoice"),
922 + "error",
923 + );
924 + }
925 + },
926 + // Available for every booking, paid or not: an unpaid booking renders a
927 + // pro-forma invoice with the gateway's payment instructions.
928 + condition: () => can("yatra_view_bookings"),
929 + },
930 + {
931 + key: "resend_confirmation",
932 + label: __("Resend confirmation email"),
933 + icon: <Mail className="w-4 h-4" />,
934 + onClick: (booking: Booking) =>
935 + resendBookingEmail(booking.id, "confirmation"),
936 + condition: (booking: Booking) =>
937 + can("yatra_edit_bookings") && !!booking.customer_email,
938 + },
939 + {
940 + key: "resend_payment",
941 + label: __("Resend payment email"),
942 + icon: <Mail className="w-4 h-4" />,
943 + onClick: (booking: Booking) =>
944 + resendBookingEmail(booking.id, "payment_confirmation"),
945 + // Only when a payment has actually been recorded.
946 + condition: (booking: Booking) =>
947 + can("yatra_edit_bookings") &&
948 + !!booking.customer_email &&
949 + Number(booking.amount_paid || 0) > 0,
950 + },
859 951 {
860 952 key: "delete",
861 953 label: __("Delete"),
862 954 icon: <Trash2 className="w-4 h-4" />,