PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.10 2.0.11 All 82 releases
yatra / app / Providers / AdminServiceProvider.php

AdminServiceProvider.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Providers/AdminServiceProvider.php

689 lines 24.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Providers;
6
7 use Yatra\Core\ServiceProvider;
8 use Yatra\Core\Container;
9 use Yatra\Core\Modules\ModuleManager;
10
11 /**
12 * Admin Service Provider
13 * Handles admin menu, assets, and pages
14 */
15 class AdminServiceProvider extends ServiceProvider
16 {
17 private const UPGRADE_TO_PRO_URL = 'https://wpyatra.com/pricing';
18
19 /**
20 * Guard so bootstrapMenuCapability() is idempotent across the two
21 * call sites (AppServiceProvider::register() — always-loaded path —
22 * and registerAdminMenu() — admin-only). add_filter() with anonymous
23 * closures does NOT dedupe, because each call creates a new closure
24 * with a distinct object identity, so without this guard the filters
25 * would run twice per cap check on admin pageviews.
26 */
27 private static bool $capabilityFiltersInstalled = false;
28
29 /**
30 * Register services
31 */
32 public function register(): void
33 {
34 // Register admin menu
35 add_action('admin_menu', [$this, 'registerAdminMenu']);
36 // After core + other Yatra submenus register; append external “Upgrade” link (avoids add_submenu_page plugin_basename mangling URLs).
37 add_action('admin_menu', [$this, 'registerUpgradeProExternalSubmenu'], 100);
38
39 add_filter('plugin_action_links_' . YATRA_PLUGIN_BASENAME, [$this, 'addPluginUpgradeLink']);
40 add_filter('plugin_row_meta', [$this, 'filterPluginRowMeta'], 10, 4);
41 // White-label-specific hooks (all_plugins rebrand, dependency name
42 // rewrite, brand-color CSS injection) live in Pro's WhiteLabel
43 // module — see yatra-pro/app/Modules/WhiteLabel/Hooks/AdminHooks.php.
44 // The plugin_row_meta filter above stays here because it serves a
45 // dual purpose (links + version row); white-label conditional logic
46 // inside it reads filter-backed brand helpers, so Pro overrides it
47 // transparently without owning the hook.
48
49 // Enqueue admin assets - use priority 20 to run after WordPress core
50 add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets'], 20);
51
52 // Add module type to script tag
53 add_filter('script_loader_tag', [$this, 'addModuleTypeToScript'], 10, 2);
54
55 // Remove admin wrapper for our page
56 add_action('admin_init', [$this, 'removeAdminWrapper']);
57
58 add_action('admin_print_styles', [$this, 'printYatraReactAdminCriticalCss'], 0);
59 add_filter('admin_body_class', [$this, 'filterYatraReactAdminBodyClass']);
60 add_action('admin_head', [$this, 'printUpgradeProAdminStyles']);
61 add_action('admin_head', [$this, 'printYatraAdminMenuIconStyles']);
62 add_action('admin_head', [$this, 'printHideYatraSetupWizardSubmenu']);
63 add_action('admin_footer', [$this, 'upgradeProSubmenuOpenInNewTab']);
64 }
65
66 /**
67 * Whether the current request is the main Yatra React admin screen (admin.php?page=yatra).
68 */
69 private function isYatraMainReactAdminScreen(): bool
70 {
71 if (!is_admin()) {
72 return false;
73 }
74
75 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- screen detection only
76 if (isset($_GET['page']) && $_GET['page'] === 'yatra') {
77 return true;
78 }
79
80 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
81
82 return $screen && $screen->id === 'toplevel_page_yatra';
83 }
84
85 /**
86 * Critical CSS + dark-mode init in &lt;head&gt; (before body) to prevent FOUC / flicker of native wp-admin chrome.
87 *
88 * Previously this lived in templates/admin.php (body), so the menu painted before hide rules applied.
89 */
90 public function printYatraReactAdminCriticalCss(): void
91 {
92 if (!$this->isYatraMainReactAdminScreen()) {
93 return;
94 }
95
96 $app_js = YATRA_PLUGIN_PATH . 'assets/admin/dist/js/app.js';
97 $has_build = file_exists($app_js);
98
99 ?>
100 <script id="yatra-admin-dark-init">
101 (function(){var d=localStorage.getItem('yatra-dark-mode');if(d==='true'){document.documentElement.classList.add('dark');}})();
102 </script>
103 <style id="yatra-react-admin-critical">
104 #wpadminbar,
105 #adminmenumain,
106 #adminmenuback,
107 #adminmenuwrap,
108 #wpfooter,
109 #screen-meta,
110 #screen-meta-links {
111 display: none !important;
112 }
113
114 #wpcontent {
115 margin-left: 0 !important;
116 padding: 0 !important;
117 }
118
119 #wpbody,
120 #wpbody-content {
121 margin: 0 !important;
122 padding: 0 !important;
123 padding-top: 0 !important;
124 }
125
126 .wrap {
127 margin: 0 !important;
128 padding: 0 !important;
129 max-width: 100% !important;
130 }
131
132 html,
133 body.wp-admin {
134 margin: 0 !important;
135 padding: 0 !important;
136 height: 100% !important;
137 overflow: <?php echo $has_build ? 'hidden' : 'auto'; ?> !important;
138 }
139
140 /* Match React Layout shell (gray-50 / gray-900) so the pre-JS phase is not a flat white flash */
141 body.yatra-react-admin-shell {
142 background: #f9fafb !important;
143 }
144
145 html.dark body.yatra-react-admin-shell {
146 background: #111827 !important;
147 }
148
149 #yatra-app-root {
150 width: 100vw;
151 height: 100vh;
152 position: fixed;
153 top: 0;
154 left: 0;
155 z-index: 99999;
156 background: #f9fafb;
157 }
158
159 html.dark #yatra-app-root {
160 background: #111827;
161 }
162
163 /* HTML/CSS-only boot UI (no JS); React replaces #yatra-app-root contents on mount */
164 .yatra-admin-boot-splash {
165 box-sizing: border-box;
166 position: absolute;
167 inset: 0;
168 display: flex;
169 align-items: center;
170 justify-content: center;
171 flex-direction: column;
172 gap: 1rem;
173 z-index: 1;
174 }
175
176 .yatra-admin-boot-spinner {
177 width: 40px;
178 height: 40px;
179 border-radius: 50%;
180 border: 3px solid rgba(79, 70, 229, 0.2);
181 border-top-color: #4f46e5;
182 animation: yatra-boot-spin 0.7s linear infinite;
183 }
184
185 html.dark .yatra-admin-boot-spinner {
186 border-color: rgba(129, 140, 248, 0.25);
187 border-top-color: #a5b4fc;
188 }
189
190 .yatra-admin-boot-text {
191 margin: 0;
192 font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
193 font-size: 14px;
194 color: #6b7280;
195 }
196
197 html.dark .yatra-admin-boot-text {
198 color: #9ca3af;
199 }
200
201 @keyframes yatra-boot-spin {
202 to {
203 transform: rotate(360deg);
204 }
205 }
206
207 .yatra-build-message {
208 padding: 40px;
209 text-align: center;
210 max-width: 800px;
211 margin: 50px auto;
212 background: #fff;
213 border: 1px solid #ddd;
214 border-radius: 8px;
215 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
216 }
217
218 .yatra-build-message h1 {
219 margin-bottom: 20px;
220 color: #333;
221 }
222
223 .yatra-build-message code {
224 display: block;
225 background: #f5f5f5;
226 padding: 15px;
227 margin: 15px 0;
228 border-radius: 4px;
229 text-align: left;
230 border-left: 4px solid #0073aa;
231 }
232 </style>
233 <?php
234 }
235
236 /**
237 * Marker class for the React shell (optional hooks / debugging).
238 *
239 * @param string $classes Space-separated classes.
240 */
241 public function filterYatraReactAdminBodyClass(string $classes): string
242 {
243 if ($this->isYatraMainReactAdminScreen()) {
244 $classes .= ' yatra-react-admin-shell';
245 }
246
247 return $classes;
248 }
249
250 /**
251 * Hide Setup Wizard in the Yatra submenu while keeping it registered (required for direct URL access).
252 */
253 public function printHideYatraSetupWizardSubmenu(): void
254 {
255 if (!apply_filters('yatra_enable_setup_wizard', true)) {
256 return;
257 }
258
259 if (!apply_filters('yatra_hide_setup_wizard_submenu', true)) {
260 return;
261 }
262
263 echo '<style id="yatra-hide-setup-submenu">'
264 . '#adminmenu .toplevel_page_yatra .wp-submenu li:has(> a[href*="page=yatra-setup"]){display:none!important;}'
265 . '</style>';
266 }
267
268 /**
269 * Size the custom Yatra menu icon (PNG) like core dashicons.
270 */
271 public function printYatraAdminMenuIconStyles(): void
272 {
273 if (!function_exists('yatra_get_brand_icon_url') || yatra_get_brand_icon_url() === '') {
274 return;
275 }
276
277 echo '<style id="yatra-admin-menu-brand-icon">#adminmenu .toplevel_page_yatra div.wp-menu-image img{opacity:1!important;width:20px!important;height:20px!important;padding:6px 0!important;object-fit:contain!important;}</style>';
278 }
279
280 /**
281 * “Upgrade to Pro” on the Plugins list (free only).
282 *
283 * @param array<int,string> $links
284 * @return array<int,string>
285 */
286 public function addPluginUpgradeLink(array $links): array
287 {
288 if (apply_filters('yatra_is_pro_active', false)) {
289 return $links;
290 }
291
292 $upgrade = sprintf(
293 '<a href="%s" class="yatra-upgrade-to-pro-link" target="_blank" rel="noopener noreferrer">%s</a>',
294 esc_url(self::UPGRADE_TO_PRO_URL),
295 esc_html__('Upgrade to Pro', 'yatra')
296 );
297
298 array_unshift($links, $upgrade);
299
300 return $links;
301 }
302
303 /**
304 * Plugins list row meta: Version, By MantraBrain, View details, Support, Plugin Homepage, Contact, Rate (5�
305 ).
306 *
307 * @param string[] $plugin_meta
308 * @param array<string, mixed> $plugin_data
309 * @return string[]
310 */
311 public function filterPluginRowMeta(array $plugin_meta, string $plugin_file, array $plugin_data, string $status): array
312 {
313 if ($plugin_file !== YATRA_PLUGIN_BASENAME) {
314 return $plugin_meta;
315 }
316
317 $is_white_label = function_exists('yatra_is_white_label_active') && yatra_is_white_label_active();
318 $brand_company = function_exists('yatra_get_brand_company') ? yatra_get_brand_company() : 'MantraBrain';
319 $brand_home = function_exists('yatra_get_brand_website_url') ? yatra_get_brand_website_url() : 'https://wpyatra.com/';
320 $brand_support = function_exists('yatra_get_brand_support_url') ? yatra_get_brand_support_url() : 'https://wordpress.org/support/plugin/yatra/reviews/?filter=5';
321
322 $home = $brand_home;
323 $org_plugin_page = 'https://wordpress.org/plugins/yatra/';
324 $support_url = $brand_support;
325 $contact_url = $brand_home;
326 $rate_url = 'https://wordpress.org/support/plugin/yatra/reviews/?filter=5';
327
328 $row = [];
329
330 if (!empty($plugin_data['Version'])) {
331 $row[] = sprintf(
332 /* translators: %s: plugin version number. */
333 __('Version %s', 'yatra'),
334 $plugin_data['Version']
335 );
336 }
337
338 $row[] = sprintf(
339 /* translators: %s: linked author name (HTML). */
340 __('By %s', 'yatra'),
341 '<a href="' . esc_url($home) . '" target="_blank" rel="noopener noreferrer">' . esc_html($brand_company) . '</a>'
342 );
343
344 // White-label sites should not link clients off to wp.org / Yatra rating
345 // pages. Show only the agency's own support + homepage links.
346 if (!$is_white_label) {
347 $row[] = sprintf(
348 '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
349 esc_url($org_plugin_page),
350 esc_html__('View details', 'yatra')
351 );
352 }
353
354 $row[] = sprintf(
355 '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
356 esc_url($support_url),
357 esc_html__('Support', 'yatra')
358 );
359
360 $row[] = sprintf(
361 '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
362 esc_url($home),
363 esc_html__('Plugin Homepage', 'yatra')
364 );
365
366 if (!$is_white_label) {
367 $row[] = sprintf(
368 '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
369 esc_url($contact_url),
370 esc_html__('Contact', 'yatra')
371 );
372
373 $row[] = sprintf(
374 '<a href="%s" target="_blank" rel="noopener noreferrer">%s <span aria-hidden="true">&#9733;&#9733;&#9733;&#9733;&#9733;</span></a>',
375 esc_url($rate_url),
376 esc_html__('Rate the plugin', 'yatra')
377 );
378 }
379
380 return $row;
381 }
382
383
384 /**
385 * Amber styling for Upgrade to Pro (admin menu + plugins list).
386 */
387 public function printUpgradeProAdminStyles(): void
388 {
389 if (apply_filters('yatra_is_pro_active', false)) {
390 return;
391 }
392
393 if (!is_admin()) {
394 return;
395 }
396
397 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
398 $onPlugins = $screen && $screen->id === 'plugins';
399
400 $pricingHref = esc_attr(self::UPGRADE_TO_PRO_URL);
401
402 // Plain link emphasis only (same layout as other submenu items / plugin row links).
403 // Bright orange, bold — reads clearly on default admin submenu and Plugins screen (no button chrome).
404 echo '<style id="yatra-upgrade-pro-styles">'
405 . '#adminmenu .toplevel_page_yatra .wp-submenu a[href="' . $pricingHref . '"]{color:#f97316!important;font-weight:700!important;}'
406 . '#adminmenu .toplevel_page_yatra .wp-submenu a[href="' . $pricingHref . '"]:hover{color:#ea580c!important;}'
407 . ($onPlugins
408 ? '.plugins-php .yatra-upgrade-to-pro-link{color:#f97316!important;font-weight:700!important;}'
409 . '.plugins-php .yatra-upgrade-to-pro-link:hover{color:#ea580c!important;}'
410 : '')
411 . '</style>';
412 }
413
414 /**
415 * Open pricing link in a new tab (submenu HTML is generated by core without target="_blank").
416 */
417 public function upgradeProSubmenuOpenInNewTab(): void
418 {
419 if (apply_filters('yatra_is_pro_active', false)) {
420 return;
421 }
422
423 if (!is_admin()) {
424 return;
425 }
426
427 $url = wp_json_encode(self::UPGRADE_TO_PRO_URL, JSON_UNESCAPED_SLASHES);
428 echo '<script>(function(){var u=' . $url . ";document.querySelectorAll('#adminmenu .toplevel_page_yatra .wp-submenu a[href=\"'+u+'\"]').forEach(function(a){a.target='_blank';a.rel='noopener noreferrer';});})();</script>";
429 }
430
431 /**
432 * Direct external submenu link (WordPress prints href from slug when it is not a registered admin page file).
433 *
434 * @global array<string,array<int,array<int,mixed>>> $submenu
435 */
436 public function registerUpgradeProExternalSubmenu(): void
437 {
438 if (apply_filters('yatra_is_pro_active', false)) {
439 return;
440 }
441
442 global $submenu;
443 if (!isset($submenu['yatra']) || !is_array($submenu['yatra'])) {
444 return;
445 }
446
447 $submenu['yatra'][] = [
448 esc_html__('Upgrade to Pro', 'yatra'),
449 'manage_options',
450 self::UPGRADE_TO_PRO_URL,
451 esc_html__('Upgrade to Pro', 'yatra'),
452 ];
453 }
454
455 /**
456 * Add type="module" attribute to yatra-admin script
457 */
458 public function addModuleTypeToScript(string $tag, string $handle): string
459 {
460 if ($handle === 'yatra-admin') {
461 // Check if type="module" is already present
462 if (strpos($tag, 'type="module"') === false && strpos($tag, "type='module'") === false) {
463 // Replace the script tag to add type="module"
464 $tag = str_replace('<script ', '<script type="module" ', $tag);
465 }
466 }
467 return $tag;
468 }
469
470 /**
471 * Remove WordPress admin wrapper for Yatra page
472 */
473 public function removeAdminWrapper(): void
474 {
475 $screen = get_current_screen();
476 if ($screen && $screen->id === 'toplevel_page_yatra') {
477 // Remove admin notices wrapper
478 remove_action('admin_notices', 'wp_admin_notices');
479 }
480 }
481
482 /**
483 * Map the Yatra menu visibility cap to `manage_options` for users
484 * who don't otherwise have it, so WP admins keep seeing the menu
485 * even when the Team & Access module (which formally defines the
486 * cap on roles) is not installed.
487 *
488 * Mechanism: hook `user_has_cap` to grant `yatra_access_admin`
489 * whenever the user already has `manage_options`. Pro/Team's
490 * Capabilities filter does the same thing via the admin-fallback
491 * path; this is a free-plugin safety-net so the cap is always
492 * truthy for site owners regardless of Pro install state.
493 *
494 * Idempotent — adding the same filter callback twice is a no-op in WP.
495 */
496 public static function bootstrapMenuCapability(): void
497 {
498 if (self::$capabilityFiltersInstalled) {
499 return;
500 }
501 self::$capabilityFiltersInstalled = true;
502
503 add_filter('user_has_cap', static function (array $allcaps, array $caps, array $args, \WP_User $user): array {
504 if (empty($allcaps['manage_options'])) return $allcaps;
505 // Only set if the cap was asked-about (avoids polluting
506 // unrelated cap checks with a key we don't need to answer).
507 foreach ($caps as $cap) {
508 if ($cap === 'yatra_access_admin') {
509 $allcaps['yatra_access_admin'] = true;
510 break;
511 }
512 }
513 return $allcaps;
514 }, 8, 4);
515
516 // Module-state cap gate. ALWAYS installed by the free plugin
517 // so it runs regardless of whether the Pro plugin / Team
518 // module are active. Strips every yatra_* cap from non-admin
519 // users UNLESS something returns true from the
520 // `yatra_team_role_enforcement_active` filter.
521 //
522 // Signal semantics (set by the Pro Team module):
523 // - Module ENABLED: signal returns TRUE → no strip, defer
524 // to Team's Capabilities filter (priority 8) for layered
525 // logic (admin fallback, expiry, revoke, role, grant).
526 // - Module DISABLED + "keep access" setting OFF (default):
527 // signal returns FALSE → strip yatra_* caps for non-admins.
528 // - Module DISABLED + "keep access" setting ON: signal
529 // returns TRUE → no strip; WP-native role machinery
530 // resolves caps from the stored role records.
531 //
532 // Why this lives in the free plugin: when Pro is deactivated,
533 // the Team module's own filter doesn't load. Without this
534 // free-side gate, users with a stored yatra_* role assignment
535 // (e.g. yatra_sales_agent) would still get their caps via
536 // WP-native role resolution — meaning deactivating Pro would
537 // NOT actually disable Yatra role-based access. This filter
538 // ensures the gate is honored regardless of Pro state.
539 //
540 // Priority 7 — runs BEFORE Team's Capabilities::filterUserHasCap
541 // (priority 8). Admin users (`manage_options`) are always
542 // exempt — they pass yatra_* caps via the admin fallback above
543 // plus per-controller manage_options short-circuits.
544 add_filter('user_has_cap', static function (array $allcaps, array $caps, array $args, \WP_User $user): array {
545 // Admin fallback FIRST — site owners always pass every
546 // yatra_* cap regardless of enforcement state OR whether
547 // the Pro Team module is installed. This is the contract
548 // that makes the free plugin usable on any site: an
549 // administrator who installs Yatra and visits Bookings /
550 // Trips / Settings must always have access without any
551 // role configuration step.
552 //
553 // We grant the SPECIFIC yatra_* caps being asked about
554 // (not a blanket pollution of every key) — keeps the
555 // $allcaps array tidy for downstream filters.
556 //
557 // CRITICAL: this admin grant must run BEFORE the
558 // enforcement-signal check below. Earlier code returned
559 // $allcaps unchanged for admins, which silently broke
560 // every controller that gates on a granular cap (e.g.
561 // `current_user_can('yatra_view_bookings')`) — admins
562 // don't have those caps by default because they're
563 // custom-registered. The old controllers worked because
564 // each one OR-ed `manage_options` into its own check; new
565 // controllers gate on the granular cap only and rely on
566 // this filter to make the admin path work uniformly.
567 if (!empty($allcaps['manage_options'])) {
568 foreach ($caps as $cap) {
569 if (\is_string($cap) && strpos($cap, 'yatra_') === 0) {
570 $allcaps[$cap] = true;
571 }
572 }
573 return $allcaps;
574 }
575
576 /**
577 * Access signal. The Pro Team module hooks this to
578 * return true when:
579 * - The Team module is enabled, OR
580 * - The module is disabled but the operator has flipped
581 * the "keep access on module disable" setting to ON.
582 *
583 * Defaults to false when no hook is installed (e.g. Pro
584 * deactivated) → we strip yatra_* caps for non-admins.
585 *
586 * @param bool $active
587 */
588 $active = (bool) apply_filters('yatra_team_role_enforcement_active', false);
589 if ($active) {
590 return $allcaps; // defer to Team module's filter
591 }
592
593 // Strip every yatra_* cap on this user. `yatra_access_admin`
594 // is stripped too — non-admin users shouldn't see the menu
595 // when the module isn't actively enforcing.
596 foreach ($allcaps as $cap => $on) {
597 if ($on && \is_string($cap) && strpos($cap, 'yatra_') === 0) {
598 $allcaps[$cap] = false;
599 }
600 }
601 return $allcaps;
602 }, 7, 4);
603
604 // NOTE: actual removal of Yatra team roles when the Team &
605 // Access module is disabled lives INSIDE the Pro Team module
606 // (see TeamModule::registerAlwaysOn → 'yatra_module_deactive'
607 // hook). That keeps role lifecycle owned by the module that
608 // creates the roles.
609 }
610
611 /**
612 * Register admin menu
613 */
614 public function registerAdminMenu(): void
615 {
616 self::bootstrapMenuCapability();
617
618 $menu_icon = (function_exists('yatra_get_brand_icon_url') && yatra_get_brand_icon_url() !== '')
619 ? yatra_get_brand_icon_url()
620 : 'dashicons-palmtree';
621
622 $brand_name = function_exists('yatra_get_brand_name') ? yatra_get_brand_name() : 'Yatra';
623
624 // Menu visibility cap. By default WP requires `manage_options`
625 // which only WP administrators have — invisible to every
626 // Yatra-only role (Accountant, Sales Agent, Guide, etc.). The
627 // Team & Access module's RoleProvisioner adds `yatra_access_admin`
628 // to every system role + grants it to admins via the user_has_cap
629 // filter. If Pro/Team isn't installed on this site, the cap
630 // doesn't exist anywhere → no role qualifies → behavior is
631 // identical to the old `manage_options` gate, and admins still
632 // pass because they have `manage_options`. So this is a strict
633 // expansion: same admins see it as before, plus team roles when
634 // Team & Access is on. We pass an OR-style check by using a
635 // filter so non-Pro sites can override.
636 $menu_cap = (string) apply_filters('yatra_admin_menu_cap', 'yatra_access_admin');
637
638 add_menu_page(
639 $brand_name,
640 $brand_name,
641 $menu_cap,
642 'yatra',
643 [$this, 'renderAdminPage'],
644 $menu_icon,
645 30
646 );
647
648 // WordPress would otherwise add a first submenu also titled "Yatra" (duplicate of the parent).
649 // A submenu with the same slug as the parent replaces that entry with a distinct label.
650 add_submenu_page(
651 'yatra',
652 /* translators: %s: branded plugin name. */
653 sprintf(__('%s Dashboard', 'yatra'), $brand_name),
654 __('Dashboard', 'yatra'),
655 $menu_cap,
656 'yatra',
657 [$this, 'renderAdminPage']
658 );
659
660 }
661
662 /**
663 * Enqueue admin assets
664 */
665 public function enqueueAdminAssets(string $hook): void
666 {
667 // Use centralized AdminAssetsProvider
668 $adminAssetsProvider = new \Yatra\Providers\AdminAssetsProvider();
669 $adminAssetsProvider->enqueueAssets($hook);
670 }
671
672
673 /**
674 * Render admin page
675 */
676 public function renderAdminPage(): void
677 {
678 // Load admin template
679 $template = YATRA_PLUGIN_PATH . 'templates/admin.php';
680
681 if (file_exists($template)) {
682 include $template;
683 } else {
684 echo '<div class="wrap"><h1>Yatra Admin</h1><p>Admin template not found.</p></div>';
685 }
686 }
687 }
688
689