PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
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 3.0.5, at app/Providers/AdminServiceProvider.php

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