| @@ -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" />, |