*/ private array $pages; /** * Registered admin page hook suffixes, for asset targeting. * * @var string[] */ private array $hook_suffixes = array(); /** * Constructor. * * @param AssetLoader $assets Asset loader. */ public function __construct( AssetLoader $assets ) { $this->assets = $assets; } /** * Build the label map (deferred so translations are loaded). * * @return array */ private function pages(): array { return array( 'profit-engine-dashboard' => __( 'Dashboard', 'profit-engine' ), 'profit-engine-reorder' => __( 'Reorder', 'profit-engine' ), 'profit-engine-purchase-orders' => __( 'Purchase Orders', 'profit-engine' ), 'profit-engine-suppliers' => __( 'Suppliers', 'profit-engine' ), 'profit-engine-products' => __( 'Products', 'profit-engine' ), 'profit-engine-profit' => __( 'Profit', 'profit-engine' ), 'profit-engine-reports' => __( 'Reports', 'profit-engine' ), 'profit-engine-notifications' => __( 'Notifications', 'profit-engine' ), 'profit-engine-settings' => __( 'Settings', 'profit-engine' ), ); } /** * Register menu + sub-menus. */ public function register(): void { $cap = Capabilities::MANAGE; $this->hook_suffixes[] = add_menu_page( __( 'ProfitEngine', 'profit-engine' ), __( 'ProfitEngine', 'profit-engine' ), $cap, self::PARENT, array( $this, 'render_app' ), $this->menu_icon(), 56 // Just below WooCommerce. ); foreach ( $this->pages() as $slug => $label ) { $this->hook_suffixes[] = add_submenu_page( self::PARENT, sprintf( /* translators: %s: sub-page name. */ __( '%s — ProfitEngine', 'profit-engine' ), wp_strip_all_tags( $label ) ), $label, $cap, $slug, array( $this, 'render_app' ) ); } // Keep our own screens free of unrelated third-party admin notices. add_action( 'admin_head', array( $this, 'suppress_foreign_notices' ) ); } /** * Remove other plugins' admin notices on ProfitEngine screens only. * * Third-party plugins hook update nags and promos onto the global notice * actions, which then render at the top of every admin page — including our * app. We clear them on our own screens (matched by hook suffix) so the * workspace stays focused; every other admin page is untouched. */ public function suppress_foreign_notices(): void { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || ! in_array( $screen->id, $this->hook_suffixes, true ) ) { return; } remove_all_actions( 'admin_notices' ); remove_all_actions( 'all_admin_notices' ); remove_all_actions( 'user_admin_notices' ); remove_all_actions( 'network_admin_notices' ); } /** * Admin-menu icon: the brand mark as a base64 SVG data URI, which WordPress * recolours to match the admin colour scheme. Falls back to a dashicon. */ private function menu_icon(): string { $file = PROFIT_ENGINE_PLUGIN_DIR . 'assets/images/menu-icon.svg'; $svg = is_readable( $file ) ? (string) file_get_contents( $file ) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local bundled file. // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- WordPress requires base64 for SVG menu icons. return '' !== $svg ? 'data:image/svg+xml;base64,' . base64_encode( $svg ) : 'dashicons-cart'; } /** * Render the SPA mount point. All sub-pages share this container. */ public function render_app(): void { if ( ! Capabilities::current_user_can() ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'profit-engine' ) ); } // Skeleton shown until the React app mounts (replaces the container's // contents). Mirrors the dashboard layout so the load feels instant. // Mirrors the app shell (sidebar + topbar + content) so nothing jumps // when React takes over. echo '
'; echo '
'; echo ''; echo '
'; echo '
'; for ( $i = 0; $i < 6; $i++ ) { echo ''; } echo '
'; echo '
'; echo '
'; } /** * Enqueue assets only on our own admin screens. * * @param string $hook_suffix Current admin page hook. */ public function maybe_enqueue_assets( string $hook_suffix ): void { if ( in_array( $hook_suffix, $this->hook_suffixes, true ) ) { $this->assets->enqueue(); } } }