PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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 2.0.1 All 84 releases
← All changes | resources/js/components/Layout.tsx +767 -234 3.0.4 → 3.0.16 View file →
@@ -9,8 +9,9 @@
9 9 import {
10 10 LayoutDashboard,
11 11 MapPin,
12 12 Calendar,
13 + CalendarClock,
13 14 CalendarDays,
14 15 Star,
15 16 BarChart3,
16 17 Settings,
@@ -25,8 +26,9 @@
25 26 TrendingUp,
26 27 List,
27 28 Activity,
28 29 Crown,
30 + Sparkles,
29 31 ChevronDown,
30 32 ChevronRight,
31 33 Mail,
32 34 Key,
@@ -34,10 +36,15 @@
34 36 Route,
35 37 BadgePercent,
36 38 Plane,
37 39 MessageSquare,
40 + MessageCircle,
41 + Network,
42 + Webhook,
43 + Users,
38 44 Puzzle,
39 45 ArrowLeft,
46 + Gift,
40 47 Loader2,
41 48 RotateCcw,
42 49 Sun,
43 50 User,
@@ -43,8 +50,9 @@
43 50 User,
44 51 } from "lucide-react";
45 52 import { __ } from "../lib/i18n";
46 53 import { Button } from "../components/ui/button";
54 +import { Tooltip } from "../components/ui/tooltip";
47 55 import { useToast } from "../components/ui/toast";
48 56 import { postFlushRewriteRules } from "../api/settings-api";
49 57
50 58 // Helper function to extract Gravatar URL from WordPress get_avatar HTML
@@ -71,8 +79,16 @@
71 79 }
72 80
73 81 import { ConditionalRender } from "../components/ui/conditional-render";
74 82 import {
83 + readMenuOverrides,
84 + readMenuOrder,
85 + readUiChrome,
86 + DEFAULT_MENU_ITEMS,
87 + type MenuOverrides,
88 +} from "../lib/sidebar-menu-defaults";
89 +import { MenuIcon } from "../lib/menu-icon";
90 +import {
75 91 Card,
76 92 CardContent,
77 93 CardHeader,
78 94 CardTitle,
@@ -85,8 +101,13 @@
85 101 type ModuleDefinition,
86 102 } from "../hooks/useModules";
87 103 import { isProPluginActive, isModuleActive } from "../lib/plugin-utils";
88 104 import { navigateMenu } from "../hooks/useNavigate";
105 +import { canCap } from "../hooks/useCapabilities";
106 +import {
107 + useNotificationCounts,
108 + SUBPAGE_TO_SECTION,
109 +} from "../hooks/useNotificationCounts";
89 110 import { InlineNotices } from "./notices/InlineNotices";
90 111
91 112 interface LayoutProps {
92 113 children: React.ReactNode;
@@ -138,8 +159,9 @@
138 159 enabled: isModulesPanelOpen,
139 160 });
140 161 const toggleModuleMutation = useToggleModule();
141 162 // Ensure modulesData is always an array before slicing
163 + // eslint-disable-next-line react-hooks/exhaustive-deps
142 164 const safeModulesData = Array.isArray(modulesData) ? modulesData : [];
143 165 const modulesPreview = useMemo<ModuleDefinition[]>(
144 166 () => safeModulesData.slice(0, 3),
145 167 [safeModulesData],
@@ -269,18 +291,31 @@
269 291 // Get current subpage and tab from URL
270 292 const currentSubpage = useMemo(() => {
271 293 const params = new URLSearchParams(window.location.search);
272 294 return params.get("subpage") || "dashboard";
295 + // eslint-disable-next-line react-hooks/exhaustive-deps
273 296 }, [urlKey]);
274 297
298 + // "New since last seen" counts for the sidebar badges (Bookings / Payments /
299 + // Abandoned Recovery). Read-only — if the request fails, no badge is shown.
300 + const { data: newCounts } = useNotificationCounts();
301 + const getNewCount = (subpage: string): number => {
302 + const section = SUBPAGE_TO_SECTION[subpage];
303 + if (!section) return 0;
304 + const value = newCounts?.[section];
305 + return typeof value === "number" && value > 0 ? value : 0;
306 + };
307 +
275 308 const currentTab = useMemo(() => {
276 309 const params = new URLSearchParams(window.location.search);
277 310 return params.get("tab") || "all";
311 + // eslint-disable-next-line react-hooks/exhaustive-deps
278 312 }, [urlKey]);
279 313
280 314 const currentAction = useMemo(() => {
281 315 const params = new URLSearchParams(window.location.search);
282 316 return params.get("action");
317 + // eslint-disable-next-line react-hooks/exhaustive-deps
283 318 }, [urlKey]);
284 319
285 320 // Check if we're on the trip form page
286 321 const isTripFormPage = useMemo(() => {
@@ -288,52 +323,29 @@
288 323 currentSubpage === "trips" &&
289 324 (currentTab === "all" || !currentTab) &&
290 325 (currentAction === "create" || currentAction === "edit")
291 326 );
327 + // eslint-disable-next-line react-hooks/exhaustive-deps
292 328 }, [currentSubpage, currentTab, currentAction, urlKey]);
293 329
294 - // Track expanded submenus - initialize based on current subpage
330 + // Track expanded submenus — start with the parent of the page being loaded
331 + // open, so a deep link or a refresh lands with the right submenu already
332 + // expanded (no flash of a collapsed menu).
333 + //
334 + // Derived from the menu definition rather than a hardcoded list of parents:
335 + // it used to name `trips` and `itinerary` explicitly, which meant every other
336 + // parent (Payments, and anything added later) stayed collapsed on refresh.
295 337 const [expandedMenus, setExpandedMenus] = useState<string[]>(() => {
296 338 const params = new URLSearchParams(window.location.search);
297 339 const subpage = params.get("subpage") || "dashboard";
298 - const menus: string[] = [];
299 340
300 - if (subpage === "trips") {
301 - menus.push("trips");
302 - }
303 -
304 - if (subpage === "itinerary") {
305 - menus.push("itinerary");
306 - }
307 -
308 - return menus;
341 + return DEFAULT_MENU_ITEMS.some(
342 + (item) => item.slug === subpage && (item.submenu?.length ?? 0) > 0,
343 + )
344 + ? [subpage]
345 + : [];
309 346 });
310 347
311 - // Auto-expand menu when on submenu pages
312 - useEffect(() => {
313 - const menusToExpand: string[] = [];
314 -
315 - if (currentSubpage === "trips") {
316 - menusToExpand.push("trips");
317 - }
318 -
319 - if (currentSubpage === "itinerary") {
320 - menusToExpand.push("itinerary");
321 - }
322 -
323 - setExpandedMenus((prev) => {
324 - // Only update if the menus to expand are different
325 - const newMenus = [...new Set([...prev, ...menusToExpand])];
326 - if (
327 - newMenus.length !== prev.length ||
328 - !newMenus.every((m) => prev.includes(m))
329 - ) {
330 - return newMenus;
331 - }
332 - return prev;
333 - });
334 - }, [currentSubpage, urlKey]);
335 -
336 348 // Get base admin URL
337 349 const baseUrl = useMemo(() => {
338 350 return window.yatraAdmin?.siteUrl
339 351 ? `${window.yatraAdmin.siteUrl}/wp-admin/admin.php?page=yatra`
@@ -340,114 +352,541 @@
340 352 : "/wp-admin/admin.php?page=yatra";
341 353 }, []);
342 354
343 355 const menuItems = useMemo(
344 - () => [
345 - { subpage: "dashboard", label: "Dashboard", icon: LayoutDashboard },
346 - {
347 - subpage: "trips",
348 - label: "Trips",
349 - icon: MapPin,
350 - submenu: [
351 - { tab: "all", label: "All Trips", icon: List },
352 - { tab: "activities", label: "Activities", icon: Activity },
353 - { tab: "destinations", label: "Destinations", icon: Route },
354 - { tab: "categories", label: "Categories", icon: FolderTree },
355 - {
356 - tab: "difficulty-levels",
357 - label: "Difficulty Levels",
358 - icon: TrendingUp,
359 - },
360 - // Availability - FREE feature, always show
361 - { tab: "availability", label: "Availability", icon: CalendarDays },
362 - // Attributes - FREE feature, always show
363 - { tab: "attributes", label: "Attributes", icon: Tag },
364 - // Additional Services - show only if Pro plugin is active and module is enabled
365 - ...(isProPluginActive() && isModuleActive("additional_services")
356 + // Every menu label is wrapped in __() with a literal string so
357 + // gettext extraction (scripts/extract-js-pot.mjs) picks them up
358 + // and Loco / Poedit / wp i18n can translate them. Previously most
359 + // were raw English literals — translations existed in the .po but
360 + // never reached the UI because there was no translation lookup at
361 + // render time. Wrapping is the WordPress-canonical way; the
362 + // pre_load_script_translations filter then delivers the locale
363 + // data to wp.i18n on every admin page load.
364 + () =>
365 + [
366 + {
367 + subpage: "dashboard",
368 + label: __("Dashboard", "yatra"),
369 + icon: LayoutDashboard,
370 + cap: "yatra_access_admin",
371 + },
372 + {
373 + subpage: "trips",
374 + label: __("Trips", "yatra"),
375 + icon: MapPin,
376 + cap: "yatra_view_trips",
377 + submenu: [
378 + { tab: "all", label: __("All Trips", "yatra"), icon: List },
379 + {
380 + tab: "activities",
381 + label: __("Activities", "yatra"),
382 + icon: Activity,
383 + },
384 + {
385 + tab: "destinations",
386 + label: __("Destinations", "yatra"),
387 + icon: Route,
388 + },
389 + {
390 + tab: "categories",
391 + label: __("Categories", "yatra"),
392 + icon: FolderTree,
393 + },
394 + {
395 + tab: "difficulty-levels",
396 + label: __("Difficulty Levels", "yatra"),
397 + icon: TrendingUp,
398 + },
399 + // Availability - FREE feature, always show
400 + {
401 + tab: "availability",
402 + label: __("Availability", "yatra"),
403 + icon: CalendarDays,
404 + },
405 + // Attributes - FREE feature, always show
406 + { tab: "attributes", label: __("Attributes", "yatra"), icon: Tag },
407 + // Additional Services - show only if Pro plugin is active and module is enabled
408 + ...(isProPluginActive() && isModuleActive("additional_services")
409 + ? [
410 + {
411 + tab: "additional-services",
412 + label: __("Additional Services", "yatra"),
413 + icon: Package,
414 + isPremium: true,
415 + },
416 + ]
417 + : []),
418 + // Trip Consent - show only if Pro plugin is active and module is enabled
419 + ...(isProPluginActive() && isModuleActive("trip_consent")
420 + ? [
421 + {
422 + tab: "trip-consent",
423 + label: __("Trip Consent", "yatra"),
424 + icon: FileSignature,
425 + isPremium: true,
426 + },
427 + ]
428 + : []),
429 + ],
430 + },
431 + {
432 + subpage: "traveler-categories",
433 + label: __("Traveler Categories", "yatra"),
434 + icon: UserCircle,
435 + cap: "yatra_manage_trip_taxonomies",
436 + },
437 + {
438 + subpage: "itinerary",
439 + label: __("Itinerary", "yatra"),
440 + icon: FileText,
441 + cap: "yatra_edit_trips",
442 + submenu: [
443 + { tab: "item-types", label: __("Item Types", "yatra"), icon: Tag },
444 + { tab: "items", label: __("Items", "yatra"), icon: Route },
445 + {
446 + tab: "itinerary",
447 + label: __("Itinerary", "yatra"),
448 + icon: FileText,
449 + },
450 + ],
451 + },
452 + // Departures - FREE feature, always show
453 + {
454 + subpage: "departures",
455 + label: __("Departures", "yatra"),
456 + icon: Calendar,
457 + cap: "yatra_view_departures",
458 + },
459 + {
460 + subpage: "discounts",
461 + label: __("Discounts", "yatra"),
462 + icon: BadgePercent,
463 + cap: "yatra_manage_discounts",
464 + },
465 + {
466 + subpage: "payments",
467 + label: __("Payments", "yatra"),
468 + icon: CreditCard,
469 + cap: "yatra_view_financial_reports",
470 + // Payments only becomes a parent when the Pro "Scheduled Payments"
471 + // module is enabled — otherwise there is nothing to nest and it stays
472 + // exactly as it was: a single item that navigates straight to the
473 + // payments list. Children are only rendered for a visible parent, so
474 + // the pair inherits the Payments capability gate.
475 + //
476 + // `undefined` rather than an empty array: the rest of this component
477 + // treats "has a submenu" as truthiness, and an empty array would read
478 + // as a parent with no matching child — which would stop the Payments
479 + // item highlighting on its own page.
480 + submenu: (window as any).yatraAdmin?.scheduledPaymentsEnabled
366 481 ? [
367 482 {
368 - tab: "additional-services",
369 - label: "Additional Services",
370 - icon: Package,
371 - isPremium: true,
483 + tab: "all",
484 + label: __("All Payments", "yatra"),
485 + icon: CreditCard,
372 486 },
373 - ]
374 - : []),
375 - // Trip Consent - show only if Pro plugin is active and module is enabled
376 - ...(isProPluginActive() && isModuleActive("trip_consent")
377 - ? [
378 487 {
379 - tab: "trip-consent",
380 - label: "Trip Consent",
381 - icon: FileSignature,
488 + // "Scheduled" alone — the parent already says Payments. This
489 + // label is also the page's heading (the top bar reads it from
490 + // the active submenu item).
491 + tab: "scheduled",
492 + label: __("Scheduled", "yatra"),
493 + icon: CalendarClock,
382 494 isPremium: true,
383 495 },
384 496 ]
385 - : []),
386 - ],
387 - },
388 - {
389 - subpage: "traveler-categories",
390 - label: "Traveler Categories",
391 - icon: UserCircle,
392 - },
393 - {
394 - subpage: "itinerary",
395 - label: "Itinerary",
396 - icon: FileText,
397 - submenu: [
398 - { tab: "item-types", label: "Item Types", icon: Tag },
399 - { tab: "items", label: "Items", icon: Route },
400 - { tab: "itinerary", label: "Itinerary", icon: FileText },
401 - ],
402 - },
403 - // Departures - FREE feature, always show
404 - { subpage: "departures", label: "Departures", icon: Calendar },
405 - { subpage: "discounts", label: "Discounts", icon: BadgePercent },
406 - { subpage: "payments", label: "Payments", icon: CreditCard },
407 - { subpage: "bookings", label: "Bookings", icon: Calendar },
408 - { subpage: "customers", label: "Customers", icon: UserCircle },
409 - { subpage: "travelers", label: "Travelers", icon: Plane },
410 - { subpage: "enquiries", label: "Enquiries", icon: MessageSquare },
411 - { subpage: "reviews", label: "Reviews", icon: Star },
412 - { subpage: "reports", label: "Reports", icon: BarChart3 },
413 - // Email — SMTP & transactional for all; Pro adds automation tabs on the same screen
414 - {
415 - subpage: "email-automation",
416 - label: __("Email", "yatra"),
417 - icon: Mail,
418 - isPremium: false,
419 - },
420 - // Abandoned Booking Recovery - show only if Pro plugin is active and module is enabled
421 - ...(isProPluginActive() && isModuleActive("abandoned_booking_recovery")
422 - ? [
423 - {
424 - subpage: "abandoned-recovery",
425 - label: "Abandoned Recovery",
426 - icon: RotateCcw,
427 - isPremium: true,
428 - },
429 - ]
430 - : []),
431 - // Dynamic Pricing - show only if Pro plugin is active and module is enabled
432 - ...(isProPluginActive() && isModuleActive("dynamic_pricing")
433 - ? [
434 - {
435 - subpage: "dynamic-pricing",
436 - label: "Dynamic Pricing",
437 - icon: TrendingUp,
438 - isPremium: true,
439 - },
440 - ]
441 - : []),
442 - { subpage: "modules", label: "Modules", icon: Puzzle },
443 - { subpage: "license", label: "License", icon: Key },
444 - { subpage: "settings", label: "Settings", icon: Settings },
445 - ],
497 + : undefined,
498 + },
499 + {
500 + subpage: "bookings",
501 + label: __("Bookings", "yatra"),
502 + icon: Calendar,
503 + cap: "yatra_view_bookings",
504 + },
505 + {
506 + subpage: "customers",
507 + label: __("Customers", "yatra"),
508 + icon: UserCircle,
509 + cap: "yatra_view_customers",
510 + },
511 + {
512 + subpage: "travelers",
513 + label: __("Travelers", "yatra"),
514 + icon: Plane,
515 + cap: "yatra_view_customers",
516 + },
517 + {
518 + subpage: "enquiries",
519 + label: __("Enquiries", "yatra"),
520 + icon: MessageSquare,
521 + cap: "yatra_view_enquiries",
522 + },
523 + {
524 + subpage: "reviews",
525 + label: __("Reviews", "yatra"),
526 + icon: Star,
527 + cap: "yatra_view_reviews",
528 + },
529 + {
530 + subpage: "reports",
531 + label: __("Reports", "yatra"),
532 + icon: BarChart3,
533 + cap: "yatra_view_operational_reports",
534 + },
535 + // Email — SMTP & transactional for all; Pro adds automation tabs on the same screen
536 + {
537 + subpage: "email-automation",
538 + label: __("Email", "yatra"),
539 + icon: Mail,
540 + isPremium: false,
541 + cap: "yatra_manage_emails",
542 + },
543 + // Abandoned Booking Recovery - show only if Pro plugin is active and module is enabled
544 + ...(isProPluginActive() && isModuleActive("abandoned_booking_recovery")
545 + ? [
546 + {
547 + subpage: "abandoned-recovery",
548 + label: __("Abandoned Recovery", "yatra"),
549 + icon: RotateCcw,
550 + isPremium: true,
551 + cap: "yatra_manage_email_automation",
552 + },
553 + ]
554 + : []),
555 + // Dynamic Pricing - show only if Pro plugin is active and module is enabled
556 + ...(isProPluginActive() && isModuleActive("dynamic_pricing")
557 + ? [
558 + {
559 + subpage: "dynamic-pricing",
560 + label: __("Dynamic Pricing", "yatra"),
561 + icon: TrendingUp,
562 + isPremium: true,
563 + cap: "yatra_edit_trips",
564 + },
565 + ]
566 + : []),
567 + {
568 + subpage: "modules",
569 + label: __("Modules", "yatra"),
570 + icon: Puzzle,
571 + cap: "yatra_manage_modules",
572 + },
573 + // White Label — only when the Agency-tier module is actually enabled.
574 + // (Agency license alone is not enough; the module toggle must be on,
575 + // otherwise users would see a link to a disabled feature. To get
576 + // here for the first time, an Agency admin enables the module from
577 + // Yatra → Modules.) The `whiteLabelEnabled` flag from
578 + // AdminAssetsProvider already implies Agency-active.
579 + // White Label — needs Agency-tier license AND module toggle on.
580 + // Mirrors the AI Assistant gate below so both menus appear /
581 + // disappear instantly when their module toggles, while staying
582 + // license-safe if the client-side `whiteLabelEnabled` flag is
583 + // ever stale or over-eager. AdminAssetsProvider seeds both
584 + // `isAgency` and `whiteLabelEnabled`; useModules.ts updates the
585 + // latter on toggle and fires `yatra-modules-updated` which bumps
586 + // navRefreshKey → this memo recomputes → menu appears.
587 + ...((window as any).yatraAdmin?.isAgency &&
588 + (window as any).yatraAdmin?.whiteLabelEnabled
589 + ? [
590 + {
591 + subpage: "white-label",
592 + label: __("White Label", "yatra"),
593 + icon: Crown,
594 + isPremium: true,
595 + cap: "yatra_manage_white_label",
596 + },
597 + ]
598 + : []),
599 + // AI Assistant — only when BOTH the license tier qualifies
600 + // (Growth or Agency) AND the module toggle is on. Operators who
601 + // disable the module shouldn't keep seeing the menu item — it
602 + // would lead to a settings page they've explicitly opted out of.
603 + // The toggle handler in useModules.ts updates
604 + // window.yatraAdmin.aiAssistantEnabled and fires
605 + // `yatra-modules-updated`, which bumps navRefreshKey so this
606 + // memo recomputes immediately without a page reload.
607 + ...((window as any).yatraAdmin?.isAiEligible &&
608 + (window as any).yatraAdmin?.aiAssistantEnabled
609 + ? [
610 + {
611 + subpage: "ai-assistant",
612 + label: __("AI Assistant", "yatra"),
613 + icon: Sparkles,
614 + isPremium: true,
615 + cap: "yatra_manage_ai",
616 + },
617 + ]
618 + : []),
619 + // WhatsApp Notifications — Growth/Agency module. Same gate
620 + // pattern as AI Assistant: needs the AI-eligibility flag (which
621 + // is the Growth-or-Agency tier check) AND the module toggle on.
622 + // useModules.ts updates `whatsappEnabled` on toggle + fires
623 + // `yatra-modules-updated` so this menu appears instantly without
624 + // a page reload.
625 + ...((window as any).yatraAdmin?.isAiEligible &&
626 + (window as any).yatraAdmin?.whatsappEnabled
627 + ? [
628 + {
629 + subpage: "whatsapp",
630 + label: __("WhatsApp", "yatra"),
631 + icon: MessageCircle,
632 + isPremium: true,
633 + cap: "yatra_manage_whatsapp",
634 + },
635 + ]
636 + : []),
637 + // Channel Manager — Agency-tier OTA distribution hub. Same gate
638 + // pattern as White Label: needs the Agency license AND the module
639 + // toggle on. useModules.ts updates `channelManagerEnabled` on
640 + // toggle + fires `yatra-modules-updated` so this menu appears
641 + // instantly without a page reload.
642 + ...((window as any).yatraAdmin?.isAgency &&
643 + (window as any).yatraAdmin?.channelManagerEnabled
644 + ? [
645 + {
646 + subpage: "channel-manager",
647 + label: __("Channel Manager", "yatra"),
648 + icon: Network,
649 + isPremium: true,
650 + cap: "yatra_manage_channel_manager",
651 + },
652 + ]
653 + : []),
654 + // Webhooks — Agency-tier outbound integration hub. Same gate
655 + // as Channel Manager / White Label: Agency license + module on.
656 + ...((window as any).yatraAdmin?.isAgency &&
657 + (window as any).yatraAdmin?.webhooksEnabled
658 + ? [
659 + {
660 + subpage: "webhooks",
661 + label: __("Webhooks", "yatra"),
662 + icon: Webhook,
663 + isPremium: true,
664 + cap: "yatra_manage_webhooks",
665 + },
666 + ]
667 + : []),
668 + // Team & Access — Agency-tier granular roles + capability-level
669 + // access + audit log. Gate matches Webhooks: Agency license +
670 + // module on. The page itself is also cap-gated server-side; the
671 + // sidebar entry is just UX.
672 + ...((window as any).yatraAdmin?.isAgency &&
673 + (window as any).yatraAdmin?.teamEnabled
674 + ? [
675 + {
676 + subpage: "team",
677 + label: __("Team & Access", "yatra"),
678 + icon: Users,
679 + isPremium: true,
680 + cap: "yatra_manage_team",
681 + },
682 + ]
683 + : []),
684 + {
685 + subpage: "license",
686 + label: __("License", "yatra"),
687 + icon: Key,
688 + cap: "yatra_manage_settings",
689 + },
690 + {
691 + subpage: "settings",
692 + label: __("Settings", "yatra"),
693 + icon: Settings,
694 + cap: "yatra_manage_settings",
695 + },
696 + ]
697 + // Cap-gate every entry. canCap() returns true for WP admins and
698 + // when the Team module isn't enabled (= admin-only mode), so this
699 + // is a no-op on every site without team management. With team
700 + // management active, non-admin team members only see the menus
701 + // their role's caps permit.
702 + //
703 + // Items without a `cap` field are always shown (default-allow).
704 + .filter((item: any) => !item.cap || canCap(item.cap)),
705 + // eslint-disable-next-line react-hooks/exhaustive-deps
446 706 [navRefreshKey],
447 707 ); // Re-calculate when navRefreshKey changes
448 708
709 + /**
710 + * Apply White Label customizations on top of the base menuItems.
711 + *
712 + * Implementation: factor menuItems into a flat atom registry, apply
713 + * label/icon/hidden overrides, then rebuild the menu tree honoring
714 + * the saved parent override (cross-parent moves) and per-parent
715 + * order map. URLs always come from the atom's intrinsic identity —
716 + * promoting a submenu to top-level does NOT change its route, so
717 + * App.tsx routing keeps working unchanged.
718 + */
719 + const brandedMenuItems = useMemo(() => {
720 + const overrides: MenuOverrides = readMenuOverrides();
721 + const orderMap = readMenuOrder();
722 +
723 + interface Atom {
724 + key: string; // "trips" or "trips.activities"
725 + defaultParent: string; // "" for top-level, parent slug for submenu
726 + label: string;
727 + iconOverride?: unknown;
728 + icon: any;
729 + // Bag of routing/render props copied from the original menu item.
730 + subpage: string;
731 + tab?: string;
732 + isPremium?: boolean;
733 + }
734 +
735 + const atoms: Atom[] = [];
736 + const atomByKey = new Map<string, Atom>();
737 +
738 + const apply = (
739 + key: string,
740 + defaultParent: string,
741 + base: any,
742 + fallbackLabel: string,
743 + ): Atom | null => {
744 + const ov = overrides[key];
745 + if (ov?.hidden) return null;
746 + const labelOverride =
747 + ov?.label && typeof ov.label === "string" && ov.label.trim() !== ""
748 + ? ov.label
749 + : null;
750 + const atom: Atom = {
751 + key,
752 + defaultParent,
753 + label: labelOverride ?? fallbackLabel,
754 + iconOverride: ov?.icon,
755 + icon: base.icon,
756 + subpage: base.subpage ?? defaultParent ?? key,
757 + tab: base.tab,
758 + isPremium: base.isPremium,
759 + };
760 + atoms.push(atom);
761 + atomByKey.set(key, atom);
762 + return atom;
763 + };
764 +
765 + for (const item of menuItems) {
766 + apply(item.subpage, "", item, item.label);
767 + if (item.submenu) {
768 + for (const sub of item.submenu as any[]) {
769 + apply(
770 + `${item.subpage}.${sub.tab}`,
771 + item.subpage,
772 + { icon: sub.icon, subpage: item.subpage, tab: sub.tab },
773 + sub.label,
774 + );
775 + }
776 + }
777 + }
778 +
779 + // Resolve each atom's effective parent (override wins; "" = top-level).
780 + const effectiveParent = (atom: Atom): string => {
781 + const ov = overrides[atom.key];
782 + if (ov && typeof ov.parent === "string") {
783 + // Only accept the override if it points at a known top-level atom
784 + // (or empty for top-level). Otherwise fall back to the default.
785 + if (ov.parent === "") return "";
786 + if (atomByKey.get(ov.parent)?.defaultParent === "") return ov.parent;
787 + }
788 + return atom.defaultParent;
789 + };
790 +
791 + // Group atoms by effective parent and order each group.
792 + const groups = new Map<string, Atom[]>();
793 + for (const atom of atoms) {
794 + const parent = effectiveParent(atom);
795 + // Self-parenting guard — should never happen but stay defensive.
796 + if (parent === atom.key) continue;
797 + const list = groups.get(parent) ?? [];
798 + list.push(atom);
799 + groups.set(parent, list);
800 + }
801 +
802 + const orderGroup = (parent: string, group: Atom[]): Atom[] => {
803 + const saved = orderMap[parent] ?? [];
804 + if (saved.length === 0) return group;
805 + const byKey = new Map(group.map((a) => [a.key, a]));
806 + const seen = new Set<string>();
807 + const ordered: Atom[] = [];
808 + for (const k of saved) {
809 + const atom = byKey.get(k);
810 + if (atom && !seen.has(k)) {
811 + ordered.push(atom);
812 + seen.add(k);
813 + }
814 + }
815 + for (const atom of group) {
816 + if (!seen.has(atom.key)) ordered.push(atom);
817 + }
818 + return ordered;
819 + };
820 +
821 + const topLevel = orderGroup("", groups.get("") ?? []);
822 + return topLevel.map((atom) => {
823 + const childAtoms = orderGroup(atom.key, groups.get(atom.key) ?? []);
824 + return {
825 + // React key. NOT `subpage`: a child promoted to top-level keeps its
826 + // parent's subpage (`payments.scheduled` → subpage `payments`), so two
827 + // siblings could share it. Duplicate keys make React reconcile the
828 + // wrong nodes and throw "removeChild ... not a child of this node".
829 + menuKey: atom.key,
830 + subpage: atom.subpage,
831 + label: atom.label,
832 + icon: atom.icon,
833 + iconOverride: atom.iconOverride,
834 + isPremium: atom.isPremium,
835 + submenu: childAtoms.length
836 + ? childAtoms.map((child) => ({
837 + // Unique per child, for the same reason as `menuKey` above:
838 + // subpage + tab can repeat once items are moved between parents.
839 + menuKey: child.key,
840 + // Submenu rendering supports both legacy intra-parent items
841 + // (matching parent's subpage) and promoted/demoted items
842 + // that point at their own subpage.
843 + tab: child.defaultParent === atom.subpage ? child.tab : undefined,
844 + label: child.label,
845 + icon: child.icon,
846 + iconOverride: child.iconOverride,
847 + // Promoted top-level items now-as-children get an absolute href.
848 + href:
849 + child.defaultParent === atom.subpage
850 + ? undefined
851 + : `${baseUrl}&subpage=${child.subpage}${child.tab ? `&tab=${child.tab}` : ""}`,
852 + subpage: child.subpage,
853 + tab_orig: child.tab,
854 + }))
855 + : undefined,
856 + };
857 + });
858 + }, [menuItems, baseUrl]);
859 +
860 + // Keep the active page's parent expanded as the URL changes. Lives here
861 + // (after `brandedMenuItems`) so it reads the menu actually being rendered —
862 + // that includes parents the menu customizer created by demoting an item into
863 + // one, which the static defaults above don't know about.
864 + useEffect(() => {
865 + const parentOfCurrentPage = brandedMenuItems.find(
866 + (item: any) =>
867 + item.subpage === currentSubpage && (item.submenu?.length ?? 0) > 0,
868 + );
869 + if (!parentOfCurrentPage) {
870 + return;
871 + }
872 +
873 + setExpandedMenus((prev) =>
874 + prev.includes(parentOfCurrentPage.subpage)
875 + ? prev
876 + : [...prev, parentOfCurrentPage.subpage],
877 + );
878 + }, [currentSubpage, urlKey, brandedMenuItems]);
879 +
880 + /** UI chrome visibility flags (version, Back to WP, Join Community). */
881 + const uiChrome = useMemo(() => readUiChrome(), []);
882 +
449 883 const isActive = (subpage: string, tab?: string) => {
884 + // The Google Calendar dashboard is reached from Settings → Integration, so
885 + // keep the Settings menu item highlighted while that page is open.
886 + if (subpage === "settings" && currentSubpage === "google-calendar") {
887 + return true;
888 + }
450 889 if (tab) {
451 890 return currentSubpage === subpage && currentTab === tab;
452 891 }
453 892 // For parent menu items, check if current subpage matches
@@ -495,33 +934,42 @@
495 934 {/* Logo */}
496 935 <div className="h-16 px-6 flex items-center justify-center border-b border-gray-200 dark:border-gray-700">
497 936 <div className="flex flex-col gap-1">
498 937 <div className="flex items-center gap-3">
499 - {window.yatraAdmin?.brandLogoUrl ? (
500 - <img
501 - src={window.yatraAdmin.brandLogoUrl}
502 - alt={__("Yatra", "yatra")}
503 - width={32}
504 - height={32}
505 - className="w-8 h-8 rounded-lg object-contain shrink-0 bg-blue-600 p-1 border border-blue-700 dark:border-blue-500"
506 - />
507 - ) : (
508 - <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shrink-0">
509 - <span className="text-white font-bold text-lg">Y</span>
510 - </div>
511 - )}
938 + {(() => {
939 + const brandName =
940 + (window as any).yatraAdmin?.brandName || "Yatra";
941 + const brandInitial = brandName.charAt(0).toUpperCase() || "Y";
942 + return (window as any).yatraAdmin?.brandLogoUrl ? (
943 + <img
944 + src={(window as any).yatraAdmin.brandLogoUrl}
945 + alt={brandName}
946 + width={32}
947 + height={32}
948 + className="w-8 h-8 rounded-lg object-contain shrink-0 bg-blue-600 p-1 border border-blue-700 dark:border-blue-500"
949 + />
950 + ) : (
951 + <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shrink-0">
952 + <span className="text-white font-bold text-lg">
953 + {brandInitial}
954 + </span>
955 + </div>
956 + );
957 + })()}
512 958 <div className="flex flex-col">
513 959 <span className="text-xl font-bold text-gray-900 dark:text-white">
514 - Yatra
960 + {(window as any).yatraAdmin?.brandName || "Yatra"}
515 961 </span>
516 - <div className="flex items-center gap-2 text-[10px] text-gray-500 dark:text-gray-400">
517 - <span>v{window.yatraAdmin?.version || "1.0.0"}</span>
518 - {(window as any).yatraAdmin?.proVersion && (
519 - <span className="px-1.5 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 rounded font-medium">
520 - Pro v{(window as any).yatraAdmin?.proVersion}
521 - </span>
522 - )}
523 - </div>
962 + {!uiChrome.hideVersion && (
963 + <div className="flex items-center gap-2 text-[10px] text-gray-500 dark:text-gray-400">
964 + <span>v{window.yatraAdmin?.version || "1.0.0"}</span>
965 + {(window as any).yatraAdmin?.proVersion && (
966 + <span className="px-1.5 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 rounded font-medium">
967 + Pro v{(window as any).yatraAdmin?.proVersion}
968 + </span>
969 + )}
970 + </div>
971 + )}
524 972 </div>
525 973 </div>
526 974 </div>
527 975 </div>
@@ -527,9 +975,9 @@
527 975 </div>
528 976
529 977 {/* Navigation + Bottom Actions */}
530 978 <nav className="flex-1 p-4 space-y-1 overflow-y-auto">
531 - {menuItems.map((item) => {
979 + {brandedMenuItems.map((item) => {
532 980 const Icon = item.icon;
533 981 const hasSubmenu = item.submenu && item.submenu.length > 0;
534 982 const isExpanded = hasSubmenu && isMenuExpanded(item.subpage);
535 983 const active = isActive(item.subpage);
@@ -534,47 +982,79 @@
534 982 const isExpanded = hasSubmenu && isMenuExpanded(item.subpage);
535 983 const active = isActive(item.subpage);
536 984
537 985 return (
538 - <div key={item.subpage}>
986 + // Keyed by the unique menu key, and by the item's shape: when a
987 + // module toggle turns a plain item into a parent (Payments →
988 + // All Payments / Scheduled) the branch below swaps an <a> for a
989 + // <button> + children, and a shape-aware key makes React remount
990 + // that subtree cleanly instead of reusing mismatched nodes.
991 + <div
992 + key={`${(item as any).menuKey ?? item.subpage}:${
993 + hasSubmenu ? "parent" : "leaf"
994 + }`}
995 + >
539 996 {hasSubmenu ? (
540 997 <>
541 998 <button
542 999 onClick={() => toggleMenu(item.subpage)}
543 1000 className={`w-full flex items-center justify-between gap-3 px-4 py-3 rounded-lg text-sm transition-colors ${
544 - active || isExpanded
1001 + active
545 1002 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium"
546 1003 : "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
547 1004 }`}
548 1005 >
549 1006 <div className="flex items-center gap-3">
550 - <Icon className="w-5 h-5" />
1007 + <MenuIcon
1008 + icon={(item as any).iconOverride}
1009 + fallback={Icon}
1010 + className="w-5 h-5"
1011 + />
551 1012 <span>{item.label}</span>
552 1013 </div>
553 - {isExpanded ? (
554 - <ChevronDown className="w-4 h-4" />
555 - ) : (
556 - <ChevronRight className="w-4 h-4" />
557 - )}
1014 + <div className="flex items-center gap-2">
1015 + {/* New-since-last-seen badge. Also rendered here, not
1016 + only on flat items: a section that gains children
1017 + (Payments) would otherwise lose its badge. */}
1018 + {!active && getNewCount(item.subpage) > 0 && (
1019 + <span
1020 + className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1.5 rounded-full bg-red-500 text-white text-[10px] font-semibold leading-none"
1021 + aria-label={`${getNewCount(item.subpage)} ${__("new", "yatra")}`}
1022 + >
1023 + {getNewCount(item.subpage) > 99
1024 + ? "99+"
1025 + : getNewCount(item.subpage)}
1026 + </span>
1027 + )}
1028 + {isExpanded ? (
1029 + <ChevronDown className="w-4 h-4" />
1030 + ) : (
1031 + <ChevronRight className="w-4 h-4" />
1032 + )}
1033 + </div>
558 1034 </button>
559 1035 {isExpanded && item.submenu && (
560 1036 <div className="ml-4 mt-1 space-y-1">
561 - {item.submenu.map((subItem) => {
562 - const subActive = isActive(
563 - item.subpage,
564 - subItem.tab,
565 - );
1037 + {item.submenu.map((subItem: any) => {
1038 + // Promoted / demoted child carries its own
1039 + // routing target via subpage + tab_orig; the
1040 + // default in-parent child uses the parent's
1041 + // subpage with its tab.
1042 + const navSubpage = subItem.subpage ?? item.subpage;
1043 + const navTab = subItem.tab ?? subItem.tab_orig;
1044 + const subActive = navTab
1045 + ? isActive(navSubpage, navTab)
1046 + : isActive(navSubpage);
566 1047 const SubIcon = subItem.icon;
567 1048 return (
568 1049 <a
569 - key={subItem.tab}
570 - href={getUrl(item.subpage, subItem.tab)}
1050 + key={
1051 + subItem.menuKey ??
1052 + `${navSubpage}.${navTab ?? ""}`
1053 + }
1054 + href={getUrl(navSubpage, navTab)}
571 1055 onClick={(e) =>
572 - handleMenuNavClick(
573 - e,
574 - item.subpage,
575 - subItem.tab,
576 - )
1056 + handleMenuNavClick(e, navSubpage, navTab)
577 1057 }
578 1058 className={`flex items-center gap-3 px-4 py-2 rounded-lg transition-colors relative ${
579 1059 subActive
580 1060 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium"
@@ -583,11 +1063,13 @@
583 1063 >
584 1064 <div className="flex items-center gap-3">
585 1065 {SubIcon && (
586 1066 <div className="w-4 h-4">
587 - {React.createElement(SubIcon, {
588 - className: "w-4 h-4",
589 - })}
1067 + <MenuIcon
1068 + icon={(subItem as any).iconOverride}
1069 + fallback={SubIcon}
1070 + className="w-4 h-4"
1071 + />
590 1072 </div>
591 1073 )}
592 1074 <span className="text-sm">
593 1075 {subItem.label}
@@ -616,11 +1098,27 @@
616 1098 : "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
617 1099 }`}
618 1100 >
619 1101 <div className="flex items-center gap-3">
620 - <Icon className="w-5 h-5" />
1102 + <MenuIcon
1103 + icon={(item as any).iconOverride}
1104 + fallback={Icon}
1105 + className="w-5 h-5"
1106 + />
621 1107 <span>{item.label}</span>
622 1108 </div>
1109 + {/* New-since-last-seen badge (Bookings / Payments /
1110 + Abandoned Recovery). Hidden while viewing that page. */}
1111 + {!active && getNewCount(item.subpage) > 0 && (
1112 + <span
1113 + className="ml-auto inline-flex items-center justify-center min-w-[18px] h-[18px] px-1.5 rounded-full bg-red-500 text-white text-[10px] font-semibold leading-none"
1114 + aria-label={`${getNewCount(item.subpage)} ${__("new", "yatra")}`}
1115 + >
1116 + {getNewCount(item.subpage) > 99
1117 + ? "99+"
1118 + : getNewCount(item.subpage)}
1119 + </span>
1120 + )}
623 1121 {item.isPremium && !isProPluginActive() && (
624 1122 <div className="absolute inset-y-0 right-2 flex items-center justify-center">
625 1123 <div className="w-4 h-4 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-white flex items-center justify-center">
626 1124 <Crown className="w-2.5 h-2.5" />
@@ -680,23 +1178,25 @@
680 1178 </div>
681 1179 )}
682 1180
683 1181 {/* Sticky bottom back link */}
684 - <div className="border-t border-gray-200 dark:border-gray-700 p-4">
685 - <a
686 - href={
687 - window.yatraAdmin?.siteUrl
688 - ? `${window.yatraAdmin.siteUrl}/wp-admin/`
689 - : "/wp-admin/"
690 - }
691 - className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg text-xs font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
692 - >
693 - <span className="inline-flex items-center gap-2">
694 - <ArrowLeft className="w-3 h-3" />
695 - <span>{__("Back to WordPress", "yatra")}</span>
696 - </span>
697 - </a>
698 - </div>
1182 + {!uiChrome.hideBackToWp && (
1183 + <div className="border-t border-gray-200 dark:border-gray-700 p-4">
1184 + <a
1185 + href={
1186 + window.yatraAdmin?.siteUrl
1187 + ? `${window.yatraAdmin.siteUrl}/wp-admin/`
1188 + : "/wp-admin/"
1189 + }
1190 + className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg text-xs font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
1191 + >
1192 + <span className="inline-flex items-center gap-2">
1193 + <ArrowLeft className="w-3 h-3" />
1194 + <span>{__("Back to WordPress", "yatra")}</span>
1195 + </span>
1196 + </a>
1197 + </div>
1198 + )}
699 1199 </aside>
700 1200
701 1201 {/* Main Content */}
702 1202 <div
@@ -704,55 +1204,88 @@
704 1204 >
705 1205 {/* Top Bar */}
706 1206 <header className="h-16 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 flex items-center">
707 1207 <div className="flex items-center justify-between w-full">
708 - <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
709 - {(() => {
710 - // Show specific text for trip form page
711 - if (isTripFormPage) {
712 - return currentAction === "create"
713 - ? "Create Trip"
714 - : "Edit Trip";
715 - }
1208 + <div className="flex items-center gap-3 flex-wrap min-w-0">
1209 + <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
1210 + {(() => {
1211 + // Show specific text for trip form page
1212 + if (isTripFormPage) {
1213 + return currentAction === "create"
1214 + ? "Create Trip"
1215 + : "Edit Trip";
1216 + }
716 1217
717 - const activeItem = menuItems.find((item) =>
718 - isActive(item.subpage),
719 - );
720 - if (activeItem?.submenu && currentTab) {
721 - const activeSubItem = activeItem.submenu.find(
722 - (sub) => sub.tab === currentTab,
1218 + const activeItem = brandedMenuItems.find((item) =>
1219 + isActive(item.subpage),
723 1220 );
724 - return activeSubItem?.label || activeItem.label;
725 - }
726 - return activeItem?.label || "Dashboard";
727 - })()}
728 - </h1>
1221 + if (activeItem?.submenu && currentTab) {
1222 + const activeSubItem = activeItem.submenu.find(
1223 + (sub) => sub.tab === currentTab,
1224 + );
1225 + return activeSubItem?.label || activeItem.label;
1226 + }
1227 + return activeItem?.label || "Dashboard";
1228 + })()}
1229 + </h1>
729 1230
1231 + {/* Conversion pill — visible only on free-plugin installs. */}
1232 + {/* Hidden as soon as the Pro plugin is active. Single edit */}
1233 + {/* in this top-bar surfaces it on every admin page. */}
1234 + {/* Gift icon leads on the "free" promise (operators read */}
1235 + {/* the icon before the text). Real Tooltip component */}
1236 + {/* instead of the browser `title` attribute so the hint */}
1237 + {/* matches the rest of the admin's hover-help style. */}
1238 + {!isProPluginActive() && (
1239 + <Tooltip
1240 + side="bottom"
1241 + content={__(
1242 + "Spin up a free Pro trial site in one click — no credit card required.",
1243 + "yatra",
1244 + )}
1245 + >
1246 + <a
1247 + href="https://try.wpyatra.com/try-yatra-pro/"
1248 + target="_blank"
1249 + rel="noopener noreferrer"
1250 + className="inline-flex items-center gap-1.5 rounded-full bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-900 ring-1 ring-amber-200 hover:bg-amber-100 hover:ring-amber-300 dark:bg-amber-950/40 dark:text-amber-100 dark:ring-amber-800 dark:hover:bg-amber-900/50 transition-colors"
1251 + >
1252 + <Gift className="h-3.5 w-3.5" />
1253 + {__("Try Yatra Pro for free", "yatra")}
1254 + </a>
1255 + </Tooltip>
1256 + )}
1257 + </div>
1258 +
730 1259 <div className="flex items-center gap-4">
731 1260 {/* Back to WordPress button */}
732 - <a
733 - href={
734 - window.yatraAdmin?.siteUrl
735 - ? `${window.yatraAdmin.siteUrl}/wp-admin/`
736 - : "/wp-admin/"
737 - }
738 - target="_blank"
739 - rel="noopener noreferrer"
740 - className="inline-flex items-center gap-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
741 - >
742 - <ArrowLeft className="w-4 h-4" />
743 - {__("Back to WordPress", "yatra")}
744 - </a>
1261 + {!uiChrome.hideBackToWp && (
1262 + <a
1263 + href={
1264 + window.yatraAdmin?.siteUrl
1265 + ? `${window.yatraAdmin.siteUrl}/wp-admin/`
1266 + : "/wp-admin/"
1267 + }
1268 + target="_blank"
1269 + rel="noopener noreferrer"
1270 + className="inline-flex items-center gap-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
1271 + >
1272 + <ArrowLeft className="w-4 h-4" />
1273 + {__("Back to WordPress", "yatra")}
1274 + </a>
1275 + )}
745 1276
746 - <a
747 - href="https://www.facebook.com/groups/yatrawordpressplugin"
748 - target="_blank"
749 - rel="noopener noreferrer"
750 - className="inline-flex items-center gap-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
751 - >
752 - <MessageSquare className="w-4 h-4 shrink-0" aria-hidden />
753 - {__("Join Community", "yatra")}
754 - </a>
1277 + {!uiChrome.hideJoinCommunity && (
1278 + <a
1279 + href="https://www.facebook.com/groups/yatrawordpressplugin"
1280 + target="_blank"
1281 + rel="noopener noreferrer"
1282 + className="inline-flex items-center gap-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
1283 + >
1284 + <MessageSquare className="w-4 h-4 shrink-0" aria-hidden />
1285 + {__("Join Community", "yatra")}
1286 + </a>
1287 + )}
755 1288
756 1289 {/* Flush permalink (same API as Settings → Permalink); icon + native title for tooltip */}
757 1290 <Button
758 1291 type="button"