import React, { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { apiClient } from "../lib/api-client"; import { API_ENDPOINTS } from "../lib/api-endpoints"; import { __ } from "../lib/i18n"; import { toDateValue } from "../lib/dateFormat"; import { LayoutDashboard, Calendar, CreditCard, User, FileText, LogOut, ShieldCheck, Heart, Package, DollarSign, } from "lucide-react"; import type { Section, Booking, Payment, TravelDocument, CustomerProfile, } from "./account/types"; import { formatDate, getYatraAccountPageGlobals, phoneToTelHref, currency as formatAccountCurrency, } from "./account/utils"; import Dashboard from "./account/Dashboard"; import Bookings from "./account/Bookings"; import Payments from "./account/Payments"; import Documents from "./account/Documents"; import Profile from "./account/Profile"; import SavedTrips from "./account/SavedTrips"; import { useToast } from "../components/ui/toast"; const navigation: Array<{ id: Section; label: string; icon: React.ElementType; }> = [ { id: "dashboard", label: __("Dashboard", "yatra"), icon: LayoutDashboard }, { id: "bookings", label: __("Bookings", "yatra"), icon: Calendar }, { id: "payments", label: __("Payments", "yatra"), icon: CreditCard }, { id: "documents", label: __("Documents", "yatra"), icon: FileText }, { id: "saved-trips", label: __("Saved Trips", "yatra"), icon: Heart }, { id: "profile", label: __("Profile", "yatra"), icon: User }, ]; const AccountPage: React.FC = () => { const accountShell = useMemo(() => getYatraAccountPageGlobals(), []); const { showToast } = useToast(); const accountNavigation = React.useMemo(() => { const wl = typeof window !== "undefined" && !!( window as unknown as { yatraAccountPage?: { wishlistEnabled?: boolean }; } ).yatraAccountPage?.wishlistEnabled; return navigation.filter((n) => n.id !== "saved-trips" || wl); }, []); // Track URL changes const [urlKey, setUrlKey] = useState(0); React.useEffect(() => { const handleLocationChange = () => { setUrlKey((prev) => prev + 1); }; // Listen for popstate (back/forward button) window.addEventListener("popstate", handleLocationChange); // Also check periodically (fallback for direct navigation) const interval = setInterval(() => { const currentSearch = window.location.search; if (currentSearch !== (window as any).__lastAccountSearch) { (window as any).__lastAccountSearch = currentSearch; handleLocationChange(); } }, 100); return () => { window.removeEventListener("popstate", handleLocationChange); clearInterval(interval); }; }, []); // Get section from URL parameter, localStorage, or default to 'dashboard' const getSectionFromUrl = (): Section => { if (typeof window !== "undefined") { const wl = !!( window as unknown as { yatraAccountPage?: { wishlistEnabled?: boolean }; } ).yatraAccountPage?.wishlistEnabled; const params = new URLSearchParams(window.location.search); const tab = params.get("tab"); if (tab === "saved-trips" && !wl) { return "dashboard"; } if ( tab && [ "dashboard", "bookings", "payments", "documents", "profile", "saved-trips", ].includes(tab) ) { return tab as Section; } // Fallback to localStorage const saved = localStorage.getItem("yatra-account-active-section"); const wlSaved = !!( window as unknown as { yatraAccountPage?: { wishlistEnabled?: boolean }; } ).yatraAccountPage?.wishlistEnabled; if (saved === "saved-trips" && !wlSaved) { return "dashboard"; } if ( saved && [ "dashboard", "bookings", "payments", "documents", "profile", "saved-trips", ].includes(saved) ) { return saved as Section; } } return "dashboard"; }; const [section, setSection] = useState
(getSectionFromUrl); // Cross-section booking selection. Lifted here from Bookings.tsx so // Dashboard's upcoming-trip cards can deep-link straight into the // booking detail screen — click on the dashboard card sets this id // AND switches to the "bookings" section; the Bookings component // honours `initialBookingId` on mount/update to open the detail view. const [pendingBookingId, setPendingBookingId] = useState(null); // Update section when URL changes React.useEffect(() => { const newSection = getSectionFromUrl(); setSection(newSection); }, [urlKey]); // Surface the result of an email-change confirmation. The confirmation link // (from WordPress-style verification email) redirects here with // ?email_change=success|error; show feedback, open the Profile tab, then // strip the param so a refresh doesn't repeat the toast. React.useEffect(() => { if (typeof window === "undefined") { return; } const params = new URLSearchParams(window.location.search); const outcome = params.get("email_change"); if (outcome !== "success" && outcome !== "error") { return; } if (outcome === "success") { showToast(__("Your email address has been updated.", "yatra"), "success"); setSection("profile"); } else { showToast( __( "This email confirmation link is invalid or has expired.", "yatra", ), "error", ); } params.delete("email_change"); const query = params.toString(); const cleanUrl = `${window.location.pathname}${query ? `?${query}` : ""}`; window.history.replaceState({}, "", cleanUrl); (window as any).__lastAccountSearch = window.location.search; // Run once on mount. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Update URL and localStorage when section changes const handleSectionChange = (newSection: Section) => { setSection(newSection); if (typeof window !== "undefined") { // Update URL const urlParams = new URLSearchParams(window.location.search); urlParams.set("tab", newSection); const newUrl = `${window.location.pathname}?${urlParams.toString()}`; window.history.pushState({}, "", newUrl); // Save to localStorage localStorage.setItem("yatra-account-active-section", newSection); // Trigger URL change event window.dispatchEvent(new PopStateEvent("popstate")); } }; // Open a specific booking from anywhere (currently used by the // Dashboard's "Upcoming Trips" cards). Switches to the bookings tab // and seeds the id so Bookings opens straight into the detail view. const handleBookingOpen = (bookingId: number) => { setPendingBookingId(bookingId); handleSectionChange("bookings"); }; // State moved to individual components const unwrapArrayResponse = (response: any): any[] => { if (!response) return []; // Common WP pattern: { success: true, data: [...] } if ( typeof response === "object" && response.success === true && Array.isArray(response.data) ) { return response.data; } // Nested wrapper pattern: { data: { success: true, data: [...] } } if ( typeof response === "object" && response.data && typeof response.data === "object" ) { const inner = response.data; if (inner.success === true && Array.isArray(inner.data)) { return inner.data; } if (Array.isArray(inner)) { return inner; } } // Direct array if (Array.isArray(response)) { return response; } // Some endpoints may return {data: [...]} without success if (typeof response === "object" && Array.isArray(response.data)) { return response.data; } return []; }; const { data: profile, isLoading: isLoadingProfile } = useQuery({ queryKey: ["account-profile"], queryFn: async () => { try { const response = await apiClient.get(API_ENDPOINTS.CUSTOMER_ME); const raw = response && typeof response === "object" && "data" in response && response.data && typeof response.data === "object" ? response.data : response; if ( !raw || typeof raw !== "object" || (!("id" in raw) && !("user_id" in raw) && !("email" in raw)) ) { return null; } const row = raw as CustomerProfile & { created_at?: string }; const fromParts = [row.first_name, row.last_name] .filter(Boolean) .join(" ") .trim(); const name = (typeof row.name === "string" && row.name.trim()) || fromParts || (typeof row.email === "string" ? row.email.split("@")[0] || "" : "") || ""; return { ...row, name, registered_at: row.registered_at || row.created_at || "", } as CustomerProfile; } catch (error) { console.error("Error fetching profile:", error); return null; } }, refetchOnMount: "always", }); const displayProfile = profile; // Fetch bookings const { data: bookings = [] } = useQuery({ queryKey: ["account-bookings"], queryFn: async () => { try { const response = await apiClient.get( API_ENDPOINTS.CUSTOMER_MY_BOOKINGS, ); return unwrapArrayResponse(response) as Booking[]; } catch (error) { console.error("Error fetching bookings:", error); return []; } }, refetchOnMount: "always", }); // Fetch payments const { data: payments = [] } = useQuery({ queryKey: ["account-payments"], queryFn: async () => { try { const response = await apiClient.get( API_ENDPOINTS.CUSTOMER_MY_PAYMENTS, ); return unwrapArrayResponse(response) as Payment[]; } catch (error) { console.error("Error fetching payments:", error); return []; } }, refetchOnMount: "always", }); // Fetch documents const { data: documents = [] } = useQuery({ queryKey: ["account-documents"], queryFn: async () => { try { const response = await apiClient.get( API_ENDPOINTS.CUSTOMER_MY_DOCUMENTS, ); return unwrapArrayResponse(response) as TravelDocument[]; } catch (error) { console.error("Error fetching documents:", error); return []; } }, refetchOnMount: "always", }); // Notifications (empty for now) const notifications: any[] = []; // Fetch saved trips const wishlistEnabled = typeof window !== "undefined" && !!( window as unknown as { yatraAccountPage?: { wishlistEnabled?: boolean } } ).yatraAccountPage?.wishlistEnabled; const { data: savedTripsData, isLoading: isLoadingSavedTrips } = useQuery({ queryKey: ["account-saved-trips"], enabled: wishlistEnabled, queryFn: async () => { try { const response = await apiClient.get(API_ENDPOINTS.SAVED_TRIPS); // WordPress REST API returns: {success: true, data: [...]} // apiClient might wrap it in response.data let trips = []; if (response && typeof response === "object") { // Check if response has a data property (apiClient wrapper) if (response.data && typeof response.data === "object") { // Check for success property first (WordPress REST API format) if ( response.data.success === true && Array.isArray(response.data.data) ) { trips = response.data.data; } // Direct data property else if (Array.isArray(response.data.data)) { trips = response.data.data; } // Direct array in response.data else if (Array.isArray(response.data)) { trips = response.data; } } // Check for success property directly else if ( response.success === true && Array.isArray(response.data) ) { trips = response.data; } // Direct data property else if (Array.isArray(response.data)) { trips = response.data; } // Direct array response else if (Array.isArray(response)) { trips = response; } } // Debug: log first trip to see structure if (trips.length > 0) { } return trips; } catch (error) { console.error("Error fetching saved trips:", error); return []; } }, }); const savedTrips = Array.isArray(savedTripsData) ? savedTripsData : []; // Booking details fetching moved to Bookings component // formatDate / site currency from ./account/utils const stats = useMemo(() => { const outstanding = payments .filter((p: Payment) => p.status === "pending") .reduce((sum: number, payment: Payment) => sum + payment.amount, 0); const upcoming = bookings.filter( (b: Booking) => toDateValue(b.travel_date) > new Date(), ).length; // Calculate from real data const totalSpent = displayProfile?.total_spent ?? 0; const totalBookings = bookings.length; return [ { label: __("Total Bookings", "yatra"), value: displayProfile?.total_bookings ?? totalBookings, icon: Package, badge: displayProfile?.loyalty_tier || "", }, { label: __("Upcoming Trips", "yatra"), value: upcoming, icon: Calendar, }, { label: __("Outstanding Balance", "yatra"), value: formatAccountCurrency(outstanding), icon: DollarSign, }, { label: __("Total Spent", "yatra"), value: formatAccountCurrency(totalSpent), icon: ShieldCheck, }, ]; }, [bookings, payments, displayProfile]); // getBadge moved to ./account/utils // Old render functions removed - now using components from ./account/ const renderSection = () => { switch (section) { case "dashboard": return ( handleSectionChange(section as Section) } onBookingOpen={handleBookingOpen} /> ); case "bookings": return ( handleSectionChange(section as Section) } initialBookingId={pendingBookingId} onBookingIdConsumed={() => setPendingBookingId(null)} /> ); case "payments": return ( handleSectionChange(section as Section) } /> ); case "documents": return ; case "saved-trips": if (!wishlistEnabled) { return null; } return ( ); case "profile": return ( ); default: return null; } }; return (

{displayProfile?.registered_at ? formatDate(displayProfile.registered_at) : ""}

{__("Hello,", "yatra")}{" "} {displayProfile?.name || __("Guest", "yatra")}

{__( "Manage bookings, payments, and documents – everything for your adventures in one place.", "yatra", )}

{ const url = accountShell.logoutUrl; if (url) { window.location.href = url; } }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); const url = accountShell.logoutUrl; if (url) { window.location.href = url; } } }} className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-200 dark:border-gray-600 cursor-pointer text-sm" > {__("Logout", "yatra")}
{isLoadingProfile && !profile ? (
{/* Header Skeleton */}
{/* Stats Skeleton */}
{[1, 2, 3, 4, 5].map((i) => (
))}
{/* Content Cards Skeleton */}
{[1, 2, 3].map((i) => (
))}
) : ( renderSection() )}
); }; export default AccountPage;