import React, { useState, useMemo, useEffect, useRef, useCallback, } from "react"; import { useMutation } from "@tanstack/react-query"; import { LayoutDashboard, MapPin, Calendar, CalendarDays, Star, BarChart3, Settings, Wrench, Moon, FileText, CreditCard, Package, UserCircle, FolderTree, Tag, TrendingUp, List, Activity, Crown, ChevronDown, ChevronRight, Mail, Key, FileSignature, Route, BadgePercent, Plane, MessageSquare, Puzzle, ArrowLeft, Loader2, RotateCcw, Sun, User, } from "lucide-react"; import { __ } from "../lib/i18n"; import { Button } from "../components/ui/button"; import { useToast } from "../components/ui/toast"; import { postFlushRewriteRules } from "../api/settings-api"; // Helper function to extract Gravatar URL from WordPress get_avatar HTML function extractGravatarUrl(avatarHtml: string, size: number): string { if (!avatarHtml) { return `https://www.gravatar.com/avatar/00000000000000000000000000000000?s=${size}&d=identicon&r=pg`; } // Extract src attribute from img tag const imgMatch = avatarHtml.match(/]+src=["']([^"']+)["']/i); if (imgMatch && imgMatch[1]) { // Replace size parameter if needed return imgMatch[1].replace(/s=\d+/, `s=${size}`); } // Fallback to default return `https://www.gravatar.com/avatar/00000000000000000000000000000000?s=${size}&d=identicon&r=pg`; } // Helper function to get Gravatar URL using WordPress data function getGravatarUrl(size: number): string { const avatarHtml = (window as any)?.yatraAdmin?.currentUserAvatar || ""; return extractGravatarUrl(avatarHtml, size); } import { ConditionalRender } from "../components/ui/conditional-render"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "../components/ui/card"; import { Badge } from "../components/ui/badge"; import { useModulesQuery, useToggleModule, type ModuleDefinition, } from "../hooks/useModules"; import { isProPluginActive, isModuleActive } from "../lib/plugin-utils"; import { navigateMenu } from "../hooks/useNavigate"; import { InlineNotices } from "./notices/InlineNotices"; interface LayoutProps { children: React.ReactNode; } const Layout: React.FC = ({ children }) => { // Load dark mode preference from localStorage const [darkMode, setDarkMode] = useState(() => { const saved = localStorage.getItem("yatra-dark-mode"); return saved === "true"; }); // Apply dark mode to document on mount and when it changes useEffect(() => { const root = document.documentElement; if (darkMode) { root.classList.add("dark"); localStorage.setItem("yatra-dark-mode", "true"); } else { root.classList.remove("dark"); localStorage.setItem("yatra-dark-mode", "false"); } }, [darkMode]); const [isModulesPanelOpen, setIsModulesPanelOpen] = useState(false); const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false); const modulesPanelRef = useRef(null); const userDropdownRef = useRef(null); // License status state for real-time updates const [licenseStatus, setLicenseStatus] = useState( (window as any).yatraAdmin?.licenseStatus || null, ); const { showToast } = useToast(); const flushRewriteRulesMutation = useMutation({ mutationFn: async () => postFlushRewriteRules(), onSuccess: () => { showToast(__("Rewrite rules flushed successfully", "yatra"), "success"); }, onError: (error: any) => { showToast( error?.message || __("Failed to flush rewrite rules", "yatra"), "error", ); }, }); const { data: modulesData, isLoading: isLoadingModules } = useModulesQuery({ enabled: isModulesPanelOpen, }); const toggleModuleMutation = useToggleModule(); // Ensure modulesData is always an array before slicing const safeModulesData = Array.isArray(modulesData) ? modulesData : []; const modulesPreview = useMemo( () => safeModulesData.slice(0, 3), [safeModulesData], ); const handleQuickToggle = (module: ModuleDefinition, enabled: boolean) => { toggleModuleMutation.mutate({ slug: module.slug, enabled, name: module.name, }); }; // Track URL changes to update menu state const [urlKey, setUrlKey] = useState(0); const [navRefreshKey, setNavRefreshKey] = useState(0); /** SPA: avoid full reload — keeps PHP boot splash from showing on every sidebar click */ const handleMenuNavClick = useCallback( (e: React.MouseEvent, subpage: string, tab?: string) => { if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey || e.button !== 0) { return; } e.preventDefault(); navigateMenu(subpage, tab); }, [], ); // Close dropdowns when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( modulesPanelRef.current && !modulesPanelRef.current.contains(event.target as Node) ) { setIsModulesPanelOpen(false); } if ( userDropdownRef.current && !userDropdownRef.current.contains(event.target as Node) ) { setIsUserDropdownOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); 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).__lastSearch) { (window as any).__lastSearch = currentSearch; handleLocationChange(); } }, 100); return () => { window.removeEventListener("popstate", handleLocationChange); clearInterval(interval); }; }, []); // Listen for module updates to refresh navigation useEffect(() => { const handleModuleUpdate = () => { // Force re-render of menu items by updating navRefreshKey setNavRefreshKey((prev) => prev + 1); // Also update urlKey to ensure all memoized values refresh setUrlKey((prev) => prev + 1); }; const handleForceRefresh = () => { setNavRefreshKey((prev) => prev + 1); setUrlKey((prev) => prev + 1); }; const handleLicenseStatusUpdate = (event: any) => { const newStatus = event.detail?.status; if (newStatus) { setLicenseStatus(newStatus); } }; window.addEventListener("yatra-modules-updated", handleModuleUpdate); window.addEventListener("yatra-force-nav-refresh", handleForceRefresh); window.addEventListener( "yatra-license-status-updated", handleLicenseStatusUpdate, ); return () => { window.removeEventListener("yatra-modules-updated", handleModuleUpdate); window.removeEventListener("yatra-force-nav-refresh", handleForceRefresh); window.removeEventListener( "yatra-license-status-updated", handleLicenseStatusUpdate, ); }; }, []); useEffect(() => { if (!isModulesPanelOpen) return; const handleClickOutside = (event: MouseEvent) => { if ( modulesPanelRef.current && !modulesPanelRef.current.contains(event.target as Node) ) { setIsModulesPanelOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isModulesPanelOpen]); // Get current subpage and tab from URL const currentSubpage = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("subpage") || "dashboard"; }, [urlKey]); const currentTab = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("tab") || "all"; }, [urlKey]); const currentAction = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("action"); }, [urlKey]); // Check if we're on the trip form page const isTripFormPage = useMemo(() => { return ( currentSubpage === "trips" && (currentTab === "all" || !currentTab) && (currentAction === "create" || currentAction === "edit") ); }, [currentSubpage, currentTab, currentAction, urlKey]); // Track expanded submenus - initialize based on current subpage const [expandedMenus, setExpandedMenus] = useState(() => { const params = new URLSearchParams(window.location.search); const subpage = params.get("subpage") || "dashboard"; const menus: string[] = []; if (subpage === "trips") { menus.push("trips"); } if (subpage === "itinerary") { menus.push("itinerary"); } return menus; }); // Auto-expand menu when on submenu pages useEffect(() => { const menusToExpand: string[] = []; if (currentSubpage === "trips") { menusToExpand.push("trips"); } if (currentSubpage === "itinerary") { menusToExpand.push("itinerary"); } setExpandedMenus((prev) => { // Only update if the menus to expand are different const newMenus = [...new Set([...prev, ...menusToExpand])]; if ( newMenus.length !== prev.length || !newMenus.every((m) => prev.includes(m)) ) { return newMenus; } return prev; }); }, [currentSubpage, urlKey]); // Get base admin URL const baseUrl = useMemo(() => { return window.yatraAdmin?.siteUrl ? `${window.yatraAdmin.siteUrl}/wp-admin/admin.php?page=yatra` : "/wp-admin/admin.php?page=yatra"; }, []); const menuItems = useMemo( () => [ { subpage: "dashboard", label: "Dashboard", icon: LayoutDashboard }, { subpage: "trips", label: "Trips", icon: MapPin, submenu: [ { tab: "all", label: "All Trips", icon: List }, { tab: "activities", label: "Activities", icon: Activity }, { tab: "destinations", label: "Destinations", icon: Route }, { tab: "categories", label: "Categories", icon: FolderTree }, { tab: "difficulty-levels", label: "Difficulty Levels", icon: TrendingUp, }, // Availability - FREE feature, always show { tab: "availability", label: "Availability", icon: CalendarDays }, // Attributes - FREE feature, always show { tab: "attributes", label: "Attributes", icon: Tag }, // Additional Services - show only if Pro plugin is active and module is enabled ...(isProPluginActive() && isModuleActive("additional_services") ? [ { tab: "additional-services", label: "Additional Services", icon: Package, isPremium: true, }, ] : []), // Trip Consent - show only if Pro plugin is active and module is enabled ...(isProPluginActive() && isModuleActive("trip_consent") ? [ { tab: "trip-consent", label: "Trip Consent", icon: FileSignature, isPremium: true, }, ] : []), ], }, { subpage: "traveler-categories", label: "Traveler Categories", icon: UserCircle, }, { subpage: "itinerary", label: "Itinerary", icon: FileText, submenu: [ { tab: "item-types", label: "Item Types", icon: Tag }, { tab: "items", label: "Items", icon: Route }, { tab: "itinerary", label: "Itinerary", icon: FileText }, ], }, // Departures - FREE feature, always show { subpage: "departures", label: "Departures", icon: Calendar }, { subpage: "discounts", label: "Discounts", icon: BadgePercent }, { subpage: "payments", label: "Payments", icon: CreditCard }, { subpage: "bookings", label: "Bookings", icon: Calendar }, { subpage: "customers", label: "Customers", icon: UserCircle }, { subpage: "travelers", label: "Travelers", icon: Plane }, { subpage: "enquiries", label: "Enquiries", icon: MessageSquare }, { subpage: "reviews", label: "Reviews", icon: Star }, { subpage: "reports", label: "Reports", icon: BarChart3 }, // Email — SMTP & transactional for all; Pro adds automation tabs on the same screen { subpage: "email-automation", label: __("Email", "yatra"), icon: Mail, isPremium: false, }, // Abandoned Booking Recovery - show only if Pro plugin is active and module is enabled ...(isProPluginActive() && isModuleActive("abandoned_booking_recovery") ? [ { subpage: "abandoned-recovery", label: "Abandoned Recovery", icon: RotateCcw, isPremium: true, }, ] : []), // Dynamic Pricing - show only if Pro plugin is active and module is enabled ...(isProPluginActive() && isModuleActive("dynamic_pricing") ? [ { subpage: "dynamic-pricing", label: "Dynamic Pricing", icon: TrendingUp, isPremium: true, }, ] : []), { subpage: "modules", label: "Modules", icon: Puzzle }, { subpage: "license", label: "License", icon: Key }, { subpage: "settings", label: "Settings", icon: Settings }, ], [navRefreshKey], ); // Re-calculate when navRefreshKey changes const isActive = (subpage: string, tab?: string) => { if (tab) { return currentSubpage === subpage && currentTab === tab; } // For parent menu items, check if current subpage matches // or if any submenu item is active if (currentSubpage === subpage) { const menuItem = menuItems.find((item) => item.subpage === subpage); if (menuItem?.submenu) { // If it has submenu, check if any submenu item is active return menuItem.submenu.some((sub) => sub.tab === currentTab); } return true; } return false; }; const isMenuExpanded = (subpage: string) => { return expandedMenus.includes(subpage); }; const toggleMenu = (subpage: string) => { setExpandedMenus((prev) => prev.includes(subpage) ? prev.filter((m) => m !== subpage) : [...prev, subpage], ); }; const getUrl = (subpage: string, tab?: string) => { if (subpage === "dashboard") { return baseUrl; } if (tab) { return `${baseUrl}&subpage=${subpage}&tab=${tab}`; } return `${baseUrl}&subpage=${subpage}`; }; return (
{/* Sidebar */} {/* Main Content */}
{/* Top Bar */}

{(() => { // Show specific text for trip form page if (isTripFormPage) { return currentAction === "create" ? "Create Trip" : "Edit Trip"; } const activeItem = menuItems.find((item) => isActive(item.subpage), ); if (activeItem?.submenu && currentTab) { const activeSubItem = activeItem.submenu.find( (sub) => sub.tab === currentTab, ); return activeSubItem?.label || activeItem.label; } return activeItem?.label || "Dashboard"; })()}

{/* Back to WordPress button */} {__("Back to WordPress", "yatra")} {/* Flush Permalinks (same API as Settings → Permalink) */}
{isModulesPanelOpen && (
{__("Modules", "yatra")} {__( "Quickly enable or disable feature packs.", "yatra", )}
{isLoadingModules && (
{__("Loading modules…", "yatra")}
)} {!isLoadingModules && modulesPreview.length === 0 && (
{__("No modules found.", "yatra")}
)} {!isLoadingModules && modulesPreview.length > 0 && (
{modulesPreview.map((module) => (

{module.name} {module.is_core && ( {__("Core", "yatra")} )}

{module.enabled ? __("Enabled", "yatra") : __("Disabled", "yatra")}

))}
)}
)}
{isUserDropdownOpen && (
{__("User { e.currentTarget.style.display = "none"; e.currentTarget.parentElement!.innerHTML = ''; }} />
{(window as any)?.yatraAdmin ?.currentUserDisplayName || (window as any)?.yatraAdmin ?.currentUserLogin || "Admin"}
{(window as any)?.yatraAdmin ?.currentUserEmail || ""}
{__("Edit Profile", "yatra")} {__("Back to WordPress", "yatra")}
)}
{/* Admin notices (Review / Buy Pro, etc.) */} {/* License Warning Banner */} {isProPluginActive() && licenseStatus && licenseStatus !== "active" && (

{licenseStatus === "expired" ? "License Expired: " : licenseStatus === "invalid" ? "Invalid License: " : "License Not Activated: "} {licenseStatus === "expired" ? "Renew your license to continue receiving updates and support." : licenseStatus === "invalid" ? "Please check your license key." : "Activate your license to receive updates and support."}

handleMenuNavClick(e, "license")} className="flex-shrink-0 px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md transition-colors" > {licenseStatus === "expired" ? "Renew Now" : "Activate Now"}
)} {/* Page Content */}
{children}
); }; export default Layout;