import React, { useState, useMemo, useEffect, useRef, useCallback, } from "react"; import { useMutation } from "@tanstack/react-query"; import { LayoutDashboard, MapPin, Calendar, CalendarClock, CalendarDays, Star, BarChart3, Settings, Wrench, Moon, FileText, CreditCard, Package, UserCircle, FolderTree, Tag, TrendingUp, List, Activity, Crown, Sparkles, ChevronDown, ChevronRight, Mail, Key, FileSignature, Route, BadgePercent, Plane, MessageSquare, MessageCircle, Network, Webhook, Users, Puzzle, ArrowLeft, Gift, Loader2, RotateCcw, Sun, User, } from "lucide-react"; import { __ } from "../lib/i18n"; import { Button } from "../components/ui/button"; import { Tooltip } from "../components/ui/tooltip"; 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 { readMenuOverrides, readMenuOrder, readUiChrome, DEFAULT_MENU_ITEMS, type MenuOverrides, } from "../lib/sidebar-menu-defaults"; import { MenuIcon } from "../lib/menu-icon"; 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 { canCap } from "../hooks/useCapabilities"; import { useNotificationCounts, SUBPAGE_TO_SECTION, } from "../hooks/useNotificationCounts"; 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 // eslint-disable-next-line react-hooks/exhaustive-deps 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"; // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlKey]); // "New since last seen" counts for the sidebar badges (Bookings / Payments / // Abandoned Recovery). Read-only — if the request fails, no badge is shown. const { data: newCounts } = useNotificationCounts(); const getNewCount = (subpage: string): number => { const section = SUBPAGE_TO_SECTION[subpage]; if (!section) return 0; const value = newCounts?.[section]; return typeof value === "number" && value > 0 ? value : 0; }; const currentTab = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("tab") || "all"; // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlKey]); const currentAction = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("action"); // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlKey]); // Check if we're on the trip form page const isTripFormPage = useMemo(() => { return ( currentSubpage === "trips" && (currentTab === "all" || !currentTab) && (currentAction === "create" || currentAction === "edit") ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentSubpage, currentTab, currentAction, urlKey]); // Track expanded submenus — start with the parent of the page being loaded // open, so a deep link or a refresh lands with the right submenu already // expanded (no flash of a collapsed menu). // // Derived from the menu definition rather than a hardcoded list of parents: // it used to name `trips` and `itinerary` explicitly, which meant every other // parent (Payments, and anything added later) stayed collapsed on refresh. const [expandedMenus, setExpandedMenus] = useState(() => { const params = new URLSearchParams(window.location.search); const subpage = params.get("subpage") || "dashboard"; return DEFAULT_MENU_ITEMS.some( (item) => item.slug === subpage && (item.submenu?.length ?? 0) > 0, ) ? [subpage] : []; }); // 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( // Every menu label is wrapped in __() with a literal string so // gettext extraction (scripts/extract-js-pot.mjs) picks them up // and Loco / Poedit / wp i18n can translate them. Previously most // were raw English literals — translations existed in the .po but // never reached the UI because there was no translation lookup at // render time. Wrapping is the WordPress-canonical way; the // pre_load_script_translations filter then delivers the locale // data to wp.i18n on every admin page load. () => [ { subpage: "dashboard", label: __("Dashboard", "yatra"), icon: LayoutDashboard, cap: "yatra_access_admin", }, { subpage: "trips", label: __("Trips", "yatra"), icon: MapPin, cap: "yatra_view_trips", submenu: [ { tab: "all", label: __("All Trips", "yatra"), icon: List }, { tab: "activities", label: __("Activities", "yatra"), icon: Activity, }, { tab: "destinations", label: __("Destinations", "yatra"), icon: Route, }, { tab: "categories", label: __("Categories", "yatra"), icon: FolderTree, }, { tab: "difficulty-levels", label: __("Difficulty Levels", "yatra"), icon: TrendingUp, }, // Availability - FREE feature, always show { tab: "availability", label: __("Availability", "yatra"), icon: CalendarDays, }, // Attributes - FREE feature, always show { tab: "attributes", label: __("Attributes", "yatra"), 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", "yatra"), 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", "yatra"), icon: FileSignature, isPremium: true, }, ] : []), ], }, { subpage: "traveler-categories", label: __("Traveler Categories", "yatra"), icon: UserCircle, cap: "yatra_manage_trip_taxonomies", }, { subpage: "itinerary", label: __("Itinerary", "yatra"), icon: FileText, cap: "yatra_edit_trips", submenu: [ { tab: "item-types", label: __("Item Types", "yatra"), icon: Tag }, { tab: "items", label: __("Items", "yatra"), icon: Route }, { tab: "itinerary", label: __("Itinerary", "yatra"), icon: FileText, }, ], }, // Departures - FREE feature, always show { subpage: "departures", label: __("Departures", "yatra"), icon: Calendar, cap: "yatra_view_departures", }, { subpage: "discounts", label: __("Discounts", "yatra"), icon: BadgePercent, cap: "yatra_manage_discounts", }, { subpage: "payments", label: __("Payments", "yatra"), icon: CreditCard, cap: "yatra_view_financial_reports", // Payments only becomes a parent when the Pro "Scheduled Payments" // module is enabled — otherwise there is nothing to nest and it stays // exactly as it was: a single item that navigates straight to the // payments list. Children are only rendered for a visible parent, so // the pair inherits the Payments capability gate. // // `undefined` rather than an empty array: the rest of this component // treats "has a submenu" as truthiness, and an empty array would read // as a parent with no matching child — which would stop the Payments // item highlighting on its own page. submenu: (window as any).yatraAdmin?.scheduledPaymentsEnabled ? [ { tab: "all", label: __("All Payments", "yatra"), icon: CreditCard, }, { // "Scheduled" alone — the parent already says Payments. This // label is also the page's heading (the top bar reads it from // the active submenu item). tab: "scheduled", label: __("Scheduled", "yatra"), icon: CalendarClock, isPremium: true, }, ] : undefined, }, { subpage: "bookings", label: __("Bookings", "yatra"), icon: Calendar, cap: "yatra_view_bookings", }, { subpage: "customers", label: __("Customers", "yatra"), icon: UserCircle, cap: "yatra_view_customers", }, { subpage: "travelers", label: __("Travelers", "yatra"), icon: Plane, cap: "yatra_view_customers", }, { subpage: "enquiries", label: __("Enquiries", "yatra"), icon: MessageSquare, cap: "yatra_view_enquiries", }, { subpage: "reviews", label: __("Reviews", "yatra"), icon: Star, cap: "yatra_view_reviews", }, { subpage: "reports", label: __("Reports", "yatra"), icon: BarChart3, cap: "yatra_view_operational_reports", }, // Email — SMTP & transactional for all; Pro adds automation tabs on the same screen { subpage: "email-automation", label: __("Email", "yatra"), icon: Mail, isPremium: false, cap: "yatra_manage_emails", }, // 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", "yatra"), icon: RotateCcw, isPremium: true, cap: "yatra_manage_email_automation", }, ] : []), // Dynamic Pricing - show only if Pro plugin is active and module is enabled ...(isProPluginActive() && isModuleActive("dynamic_pricing") ? [ { subpage: "dynamic-pricing", label: __("Dynamic Pricing", "yatra"), icon: TrendingUp, isPremium: true, cap: "yatra_edit_trips", }, ] : []), { subpage: "modules", label: __("Modules", "yatra"), icon: Puzzle, cap: "yatra_manage_modules", }, // White Label — only when the Agency-tier module is actually enabled. // (Agency license alone is not enough; the module toggle must be on, // otherwise users would see a link to a disabled feature. To get // here for the first time, an Agency admin enables the module from // Yatra → Modules.) The `whiteLabelEnabled` flag from // AdminAssetsProvider already implies Agency-active. // White Label — needs Agency-tier license AND module toggle on. // Mirrors the AI Assistant gate below so both menus appear / // disappear instantly when their module toggles, while staying // license-safe if the client-side `whiteLabelEnabled` flag is // ever stale or over-eager. AdminAssetsProvider seeds both // `isAgency` and `whiteLabelEnabled`; useModules.ts updates the // latter on toggle and fires `yatra-modules-updated` which bumps // navRefreshKey → this memo recomputes → menu appears. ...((window as any).yatraAdmin?.isAgency && (window as any).yatraAdmin?.whiteLabelEnabled ? [ { subpage: "white-label", label: __("White Label", "yatra"), icon: Crown, isPremium: true, cap: "yatra_manage_white_label", }, ] : []), // AI Assistant — only when BOTH the license tier qualifies // (Growth or Agency) AND the module toggle is on. Operators who // disable the module shouldn't keep seeing the menu item — it // would lead to a settings page they've explicitly opted out of. // The toggle handler in useModules.ts updates // window.yatraAdmin.aiAssistantEnabled and fires // `yatra-modules-updated`, which bumps navRefreshKey so this // memo recomputes immediately without a page reload. ...((window as any).yatraAdmin?.isAiEligible && (window as any).yatraAdmin?.aiAssistantEnabled ? [ { subpage: "ai-assistant", label: __("AI Assistant", "yatra"), icon: Sparkles, isPremium: true, cap: "yatra_manage_ai", }, ] : []), // WhatsApp Notifications — Growth/Agency module. Same gate // pattern as AI Assistant: needs the AI-eligibility flag (which // is the Growth-or-Agency tier check) AND the module toggle on. // useModules.ts updates `whatsappEnabled` on toggle + fires // `yatra-modules-updated` so this menu appears instantly without // a page reload. ...((window as any).yatraAdmin?.isAiEligible && (window as any).yatraAdmin?.whatsappEnabled ? [ { subpage: "whatsapp", label: __("WhatsApp", "yatra"), icon: MessageCircle, isPremium: true, cap: "yatra_manage_whatsapp", }, ] : []), // Channel Manager — Agency-tier OTA distribution hub. Same gate // pattern as White Label: needs the Agency license AND the module // toggle on. useModules.ts updates `channelManagerEnabled` on // toggle + fires `yatra-modules-updated` so this menu appears // instantly without a page reload. ...((window as any).yatraAdmin?.isAgency && (window as any).yatraAdmin?.channelManagerEnabled ? [ { subpage: "channel-manager", label: __("Channel Manager", "yatra"), icon: Network, isPremium: true, cap: "yatra_manage_channel_manager", }, ] : []), // Webhooks — Agency-tier outbound integration hub. Same gate // as Channel Manager / White Label: Agency license + module on. ...((window as any).yatraAdmin?.isAgency && (window as any).yatraAdmin?.webhooksEnabled ? [ { subpage: "webhooks", label: __("Webhooks", "yatra"), icon: Webhook, isPremium: true, cap: "yatra_manage_webhooks", }, ] : []), // Team & Access — Agency-tier granular roles + capability-level // access + audit log. Gate matches Webhooks: Agency license + // module on. The page itself is also cap-gated server-side; the // sidebar entry is just UX. ...((window as any).yatraAdmin?.isAgency && (window as any).yatraAdmin?.teamEnabled ? [ { subpage: "team", label: __("Team & Access", "yatra"), icon: Users, isPremium: true, cap: "yatra_manage_team", }, ] : []), { subpage: "license", label: __("License", "yatra"), icon: Key, cap: "yatra_manage_settings", }, { subpage: "settings", label: __("Settings", "yatra"), icon: Settings, cap: "yatra_manage_settings", }, ] // Cap-gate every entry. canCap() returns true for WP admins and // when the Team module isn't enabled (= admin-only mode), so this // is a no-op on every site without team management. With team // management active, non-admin team members only see the menus // their role's caps permit. // // Items without a `cap` field are always shown (default-allow). .filter((item: any) => !item.cap || canCap(item.cap)), // eslint-disable-next-line react-hooks/exhaustive-deps [navRefreshKey], ); // Re-calculate when navRefreshKey changes /** * Apply White Label customizations on top of the base menuItems. * * Implementation: factor menuItems into a flat atom registry, apply * label/icon/hidden overrides, then rebuild the menu tree honoring * the saved parent override (cross-parent moves) and per-parent * order map. URLs always come from the atom's intrinsic identity — * promoting a submenu to top-level does NOT change its route, so * App.tsx routing keeps working unchanged. */ const brandedMenuItems = useMemo(() => { const overrides: MenuOverrides = readMenuOverrides(); const orderMap = readMenuOrder(); interface Atom { key: string; // "trips" or "trips.activities" defaultParent: string; // "" for top-level, parent slug for submenu label: string; iconOverride?: unknown; icon: any; // Bag of routing/render props copied from the original menu item. subpage: string; tab?: string; isPremium?: boolean; } const atoms: Atom[] = []; const atomByKey = new Map(); const apply = ( key: string, defaultParent: string, base: any, fallbackLabel: string, ): Atom | null => { const ov = overrides[key]; if (ov?.hidden) return null; const labelOverride = ov?.label && typeof ov.label === "string" && ov.label.trim() !== "" ? ov.label : null; const atom: Atom = { key, defaultParent, label: labelOverride ?? fallbackLabel, iconOverride: ov?.icon, icon: base.icon, subpage: base.subpage ?? defaultParent ?? key, tab: base.tab, isPremium: base.isPremium, }; atoms.push(atom); atomByKey.set(key, atom); return atom; }; for (const item of menuItems) { apply(item.subpage, "", item, item.label); if (item.submenu) { for (const sub of item.submenu as any[]) { apply( `${item.subpage}.${sub.tab}`, item.subpage, { icon: sub.icon, subpage: item.subpage, tab: sub.tab }, sub.label, ); } } } // Resolve each atom's effective parent (override wins; "" = top-level). const effectiveParent = (atom: Atom): string => { const ov = overrides[atom.key]; if (ov && typeof ov.parent === "string") { // Only accept the override if it points at a known top-level atom // (or empty for top-level). Otherwise fall back to the default. if (ov.parent === "") return ""; if (atomByKey.get(ov.parent)?.defaultParent === "") return ov.parent; } return atom.defaultParent; }; // Group atoms by effective parent and order each group. const groups = new Map(); for (const atom of atoms) { const parent = effectiveParent(atom); // Self-parenting guard — should never happen but stay defensive. if (parent === atom.key) continue; const list = groups.get(parent) ?? []; list.push(atom); groups.set(parent, list); } const orderGroup = (parent: string, group: Atom[]): Atom[] => { const saved = orderMap[parent] ?? []; if (saved.length === 0) return group; const byKey = new Map(group.map((a) => [a.key, a])); const seen = new Set(); const ordered: Atom[] = []; for (const k of saved) { const atom = byKey.get(k); if (atom && !seen.has(k)) { ordered.push(atom); seen.add(k); } } for (const atom of group) { if (!seen.has(atom.key)) ordered.push(atom); } return ordered; }; const topLevel = orderGroup("", groups.get("") ?? []); return topLevel.map((atom) => { const childAtoms = orderGroup(atom.key, groups.get(atom.key) ?? []); return { // React key. NOT `subpage`: a child promoted to top-level keeps its // parent's subpage (`payments.scheduled` → subpage `payments`), so two // siblings could share it. Duplicate keys make React reconcile the // wrong nodes and throw "removeChild ... not a child of this node". menuKey: atom.key, subpage: atom.subpage, label: atom.label, icon: atom.icon, iconOverride: atom.iconOverride, isPremium: atom.isPremium, submenu: childAtoms.length ? childAtoms.map((child) => ({ // Unique per child, for the same reason as `menuKey` above: // subpage + tab can repeat once items are moved between parents. menuKey: child.key, // Submenu rendering supports both legacy intra-parent items // (matching parent's subpage) and promoted/demoted items // that point at their own subpage. tab: child.defaultParent === atom.subpage ? child.tab : undefined, label: child.label, icon: child.icon, iconOverride: child.iconOverride, // Promoted top-level items now-as-children get an absolute href. href: child.defaultParent === atom.subpage ? undefined : `${baseUrl}&subpage=${child.subpage}${child.tab ? `&tab=${child.tab}` : ""}`, subpage: child.subpage, tab_orig: child.tab, })) : undefined, }; }); }, [menuItems, baseUrl]); // Keep the active page's parent expanded as the URL changes. Lives here // (after `brandedMenuItems`) so it reads the menu actually being rendered — // that includes parents the menu customizer created by demoting an item into // one, which the static defaults above don't know about. useEffect(() => { const parentOfCurrentPage = brandedMenuItems.find( (item: any) => item.subpage === currentSubpage && (item.submenu?.length ?? 0) > 0, ); if (!parentOfCurrentPage) { return; } setExpandedMenus((prev) => prev.includes(parentOfCurrentPage.subpage) ? prev : [...prev, parentOfCurrentPage.subpage], ); }, [currentSubpage, urlKey, brandedMenuItems]); /** UI chrome visibility flags (version, Back to WP, Join Community). */ const uiChrome = useMemo(() => readUiChrome(), []); const isActive = (subpage: string, tab?: string) => { // The Google Calendar dashboard is reached from Settings → Integration, so // keep the Settings menu item highlighted while that page is open. if (subpage === "settings" && currentSubpage === "google-calendar") { return true; } 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 */}
); })} {!isProPluginActive() && ( )} {/* Sticky bottom back link */} {!uiChrome.hideBackToWp && ( )} {/* Main Content */}
{/* Top Bar */}

{(() => { // Show specific text for trip form page if (isTripFormPage) { return currentAction === "create" ? "Create Trip" : "Edit Trip"; } const activeItem = brandedMenuItems.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"; })()}

{/* Conversion pill — visible only on free-plugin installs. */} {/* Hidden as soon as the Pro plugin is active. Single edit */} {/* in this top-bar surfaces it on every admin page. */} {/* Gift icon leads on the "free" promise (operators read */} {/* the icon before the text). Real Tooltip component */} {/* instead of the browser `title` attribute so the hint */} {/* matches the rest of the admin's hover-help style. */} {!isProPluginActive() && ( {__("Try Yatra Pro for free", "yatra")} )}
{/* Back to WordPress button */} {!uiChrome.hideBackToWp && ( {__("Back to WordPress", "yatra")} )} {!uiChrome.hideJoinCommunity && ( {__("Join Community", "yatra")} )} {/* Flush permalink (same API as Settings → Permalink); icon + native title for tooltip */}
{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;