PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.2
2.4.3 2.4.4 2.4.2 2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 All 60 releases
← All changes | includes/Admin/EasyInvoiceAdmin.php +293 -69 2.1.12 → 2.4.2 View file →
@@ -73,9 +73,9 @@
73 73
74 74 /**
75 75 * Template Builder controller instance
76 76 *
77 - * @var \EasyInvoicePro\Controllers\TemplateBuilderController
77 + * @var \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController
78 78 */
79 79 private $template_builder_controller;
80 80
81 81 /**
@@ -89,11 +89,19 @@
89 89 $this->report_controller = new ReportController();
90 90 $this->dashboard_controller = new DashboardController();
91 91 $this->payment_controller = new PaymentController();
92 92
93 - // Initialize Template Builder controller if Pro is active
94 - if (easy_invoice_has_pro() && class_exists('\EasyInvoicePro\Controllers\TemplateBuilderController')) {
95 - $this->template_builder_controller = new \EasyInvoicePro\Controllers\TemplateBuilderController();
93 + // Initialize Template Builder controller if Pro is active AND
94 + // the custom_templates addon is enabled. The addon owns this
95 + // feature — see addons/CustomTemplates/addon.php in the Pro
96 + // plugin. Constructing the controller here always-on would
97 + // register its admin_menu hook regardless of addon state.
98 + if (
99 + easy_invoice_has_pro()
100 + && \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')
101 + && class_exists('\EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController')
102 + ) {
103 + $this->template_builder_controller = new \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController();
96 104 }
97 105 }
98 106
99 107 /**
@@ -101,8 +109,9 @@
101 109 */
102 110 public function init() {
103 111 // Add menu items
104 112 add_action('admin_menu', array($this, 'registerMenuPages'));
113 + add_action('admin_menu', array($this, 'relaxMenuCapabilities'), 9999);
105 114
106 115 // Register admin assets
107 116 $admin_assets = new AdminAssets();
108 117 $admin_assets->register();
@@ -115,8 +124,11 @@
115 124
116 125 // Hide admin UI
117 126 add_action('admin_head', array($this, 'hideAdminUi'));
118 127
128 + // Body class for layout fixes (admin bar offset, header strip alignment)
129 + add_filter('admin_body_class', array($this, 'adminBodyClass'));
130 +
119 131 // Initialize controllers
120 132 $this->invoice_controller->init();
121 133 $this->quote_controller->init();
122 134 $this->client_controller->init();
@@ -129,8 +141,74 @@
129 141 add_action('easy_invoice_admin_main_content', array($this, 'mainPageContent'));
130 142 }
131 143
132 144 /**
145 + * Let an addon lower the capability a menu page demands.
146 + *
147 + * Every page is registered with `manage_options`, which is right for a
148 + * site with one administrator and wrong the moment the Team Roles addon
149 + * hands a colleague an "EI Viewer" role: WordPress refused them every
150 + * screen before the plugin's own capability checks were even consulted.
151 + * add_submenu_page() drops a page outright when the current user lacks
152 + * the capability, so the plugin's own pages pass through menuCapability()
153 + * at registration; this late pass covers pages registered elsewhere.
154 + */
155 + public function menuCapability($slug) {
156 + /** This filter is documented in relaxMenuCapabilities(). */
157 + return (string) apply_filters('easy_invoice_menu_capability', 'manage_options', (string) $slug);
158 + }
159 +
160 + public function relaxMenuCapabilities() {
161 + global $menu, $submenu;
162 + $relax = static function ($cap, $slug) {
163 + if (!is_string($slug) || 0 !== strpos($slug, 'easy-') ) {
164 + return $cap;
165 + }
166 + /**
167 + * Filter the capability required to open an Easy Invoice admin page.
168 + *
169 + * @param string $cap Capability registered for the page.
170 + * @param string $slug Page slug.
171 + */
172 + return (string) apply_filters('easy_invoice_menu_capability', $cap, $slug);
173 + };
174 + if (is_array($menu)) {
175 + foreach ($menu as $i => $item) {
176 + if (isset($item[1], $item[2])) {
177 + $menu[$i][1] = $relax($item[1], $item[2]);
178 + }
179 + }
180 + }
181 + if (is_array($submenu)) {
182 + foreach ($submenu as $parent => $items) {
183 + foreach ((array) $items as $i => $item) {
184 + if (isset($item[1], $item[2])) {
185 + $submenu[$parent][$i][1] = $relax($item[1], $item[2]);
186 + }
187 + }
188 + }
189 + }
190 + }
191 +
192 + /**
193 + * The admin menu mark: the same document-and-tick glyph as the plugin
194 + * icon, monochrome so it follows WordPress' menu colour schemes. Falls
195 + * back to a dashicon if the file is ever missing from the build.
196 + *
197 + * @return string Data URI, or a dashicon class.
198 + */
199 + public static function menuIcon() {
200 + static $icon = null;
201 + if ($icon !== null) {
202 + return $icon;
203 + }
204 + $file = EASY_INVOICE_PLUGIN_DIR . 'assets/images/menu-icon.svg';
205 + $svg = is_readable($file) ? file_get_contents($file) : '';
206 + $icon = $svg ? 'data:image/svg+xml;base64,' . base64_encode($svg) : 'dashicons-media-text';
207 + return $icon;
208 + }
209 +
210 + /**
133 211 * Register admin menu pages
134 212 */
135 213 public function registerMenuPages() {
136 214 // Main menu item
@@ -136,12 +214,12 @@
136 214 // Main menu item
137 215 add_menu_page(
138 216 __('Easy Invoice', 'easy-invoice'),
139 217 __('Easy Invoice', 'easy-invoice'),
140 - 'manage_options',
218 + $this->menuCapability(PagesSlugs::DASHBOARD),
141 219 PagesSlugs::DASHBOARD,
142 220 array($this, 'displayMainPage'),
143 - 'dashicons-media-text',
221 + self::menuIcon(),
144 222 25
145 223 );
146 224
147 225 // Dashboard submenu
@@ -148,9 +226,9 @@
148 226 add_submenu_page(
149 227 'easy-invoice',
150 228 __('Dashboard', 'easy-invoice'),
151 229 __('Dashboard', 'easy-invoice'),
152 - 'manage_options',
230 + $this->menuCapability(PagesSlugs::DASHBOARD),
153 231 PagesSlugs::DASHBOARD,
154 232 array($this, 'displayMainPage')
155 233 );
156 234
@@ -158,9 +236,9 @@
158 236 add_submenu_page(
159 237 'easy-invoice',
160 238 __('All Invoices', 'easy-invoice'),
161 239 __('All Invoices', 'easy-invoice'),
162 - 'manage_options',
240 + $this->menuCapability(PagesSlugs::ALL_INVOICES),
163 241 PagesSlugs::ALL_INVOICES,
164 242 array($this, 'displayMainPage')
165 243 );
166 244
@@ -168,9 +246,9 @@
168 246 add_submenu_page(
169 247 'easy-invoice-hidden',
170 248 __('Add New Invoice', 'easy-invoice'),
171 249 __('Add New', 'easy-invoice'),
172 - 'manage_options',
250 + $this->menuCapability(PagesSlugs::INVOICE_NEW),
173 251 PagesSlugs::INVOICE_NEW,
174 252 array($this, 'displayMainPage')
175 253 );
176 254
@@ -178,9 +256,9 @@
178 256 add_submenu_page(
179 257 'easy-invoice',
180 258 __('All Quotes', 'easy-invoice'),
181 259 __('All Quotes', 'easy-invoice'),
182 - 'manage_options',
260 + $this->menuCapability(PagesSlugs::ALL_QUOTES),
183 261 PagesSlugs::ALL_QUOTES,
184 262 array($this, 'displayMainPage')
185 263 );
186 264
@@ -188,9 +266,9 @@
188 266 add_submenu_page(
189 267 'easy-invoice-hidden',
190 268 __('Add New Quote', 'easy-invoice'),
191 269 __('Add New Quote', 'easy-invoice'),
192 - 'manage_options',
270 + $this->menuCapability(PagesSlugs::QUOTE_NEW),
193 271 PagesSlugs::QUOTE_NEW,
194 272 array($this, 'displayMainPage')
195 273 );
196 274
@@ -198,9 +276,9 @@
198 276 add_submenu_page(
199 277 'easy-invoice-hidden',
200 278 __('Payments', 'easy-invoice'),
201 279 __('Payments', 'easy-invoice'),
202 - 'manage_options',
280 + $this->menuCapability(PagesSlugs::PAYMENTS),
203 281 PagesSlugs::PAYMENTS,
204 282 array($this, 'displayMainPage')
205 283 );
206 284
@@ -208,9 +286,9 @@
208 286 add_submenu_page(
209 287 'easy-invoice-hidden',
210 288 __('Add New Payment', 'easy-invoice'),
211 289 __('Add New Payment', 'easy-invoice'),
212 - 'manage_options',
290 + $this->menuCapability(PagesSlugs::PAYMENT_NEW),
213 291 PagesSlugs::PAYMENT_NEW,
214 292 array($this, 'displayMainPage')
215 293 );
216 294
@@ -218,50 +296,66 @@
218 296 add_submenu_page(
219 297 'easy-invoice',
220 298 __('All Clients', 'easy-invoice'),
221 299 __('All Clients', 'easy-invoice'),
222 - 'edit_posts',
300 + $this->menuCapability(PagesSlugs::CLIENTS),
223 301 PagesSlugs::CLIENTS,
224 302 array($this, 'displayMainPage')
225 303 );
226 304
227 - // Item Library submenu - only show if Pro is active
228 - if (easy_invoice_has_pro()) {
305 + // Item Library + Template Builder pages are reached via the
306 + // Easy Invoice in-app sidebar (templates/main-template.php), NOT
307 + // the WP admin submenu. We still register the page slugs so the
308 + // URLs resolve, but we parent them under 'easy-invoice-hidden'
309 + // so they don't render duplicate entries in the WP submenu —
310 + // the WP submenu showed the link AND the in-app sidebar showed
311 + // the same link, giving the user two visible navigation
312 + // entry-points to the same page.
313 + //
314 + // Registration is still gated on the addon being active — if
315 + // the addon is off, the slug isn't registered, so direct-URL
316 + // access returns the standard "you do not have sufficient
317 + // permissions" page.
318 + if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')) {
229 319 add_submenu_page(
230 - 'easy-invoice',
320 + 'easy-invoice-hidden',
231 321 __('Item Library', 'easy-invoice'),
232 322 __('Item Library', 'easy-invoice'),
233 - 'manage_options',
323 + $this->menuCapability('easy-invoice-pro-item-library'),
234 324 'easy-invoice-pro-item-library',
235 325 array($this, 'displayMainPage')
236 326 );
237 -
238 - // Template Builder submenu - only show if Pro is active
327 + }
328 +
329 + if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')) {
330 + // Template listing — reachable from the in-app sidebar.
239 331 add_submenu_page(
240 - 'easy-invoice',
332 + 'easy-invoice-hidden',
241 333 __('Template Builder', 'easy-invoice'),
242 334 __('Template Builder', 'easy-invoice'),
243 - 'manage_options',
335 + $this->menuCapability('easy-invoice-templates'),
244 336 'easy-invoice-templates',
245 337 array($this, 'displayMainPage')
246 338 );
247 -
248 - // Create New Template submenu - only show if Pro is active
339 +
340 + // Create New Template — used by deep-links from inside the
341 + // builder UI.
249 342 add_submenu_page(
250 343 'easy-invoice-hidden',
251 344 __('Create New Template', 'easy-invoice'),
252 345 __('Create New', 'easy-invoice'),
253 - 'manage_options',
346 + $this->menuCapability('easy-invoice-templates-new'),
254 347 'easy-invoice-templates-new',
255 348 array($this, 'displayMainPage')
256 349 );
257 -
258 - // Template Builder Page submenu - only show if Pro is active (hidden page)
350 +
351 + // Template Builder canvas — full-screen builder, reached
352 + // from the listing page's Edit / New buttons.
259 353 add_submenu_page(
260 354 'easy-invoice-hidden',
261 355 __('Template Builder Page', 'easy-invoice'),
262 356 __('Template Builder Page', 'easy-invoice'),
263 - 'manage_options',
357 + $this->menuCapability('easy-invoice-template-builder'),
264 358 'easy-invoice-template-builder',
265 359 array($this, 'displayMainPage')
266 360 );
267 361 }
@@ -270,9 +364,9 @@
270 364 add_submenu_page(
271 365 'easy-invoice-hidden', // Use main menu as parent to avoid title issues
272 366 __('Edit Client', 'easy-invoice'),
273 367 __('Edit Client', 'easy-invoice'),
274 - 'manage_options',
368 + $this->menuCapability(PagesSlugs::CLIENT_EDIT),
275 369 PagesSlugs::CLIENT_EDIT,
276 370 array($this, 'displayMainPage')
277 371 );
278 372
@@ -279,9 +373,9 @@
279 373 add_submenu_page(
280 374 'easy-invoice-hidden', // Use main menu as parent to avoid title issues
281 375 __('View Client', 'easy-invoice'),
282 376 __('View Client', 'easy-invoice'),
283 - 'manage_options',
377 + $this->menuCapability(PagesSlugs::CLIENT_VIEW),
284 378 PagesSlugs::CLIENT_VIEW,
285 379 array($this, 'displayMainPage')
286 380 );
287 381
@@ -288,9 +382,9 @@
288 382 add_submenu_page(
289 383 'easy-invoice-hidden', // Use main menu as parent to avoid title issues
290 384 __('Preview Invoice', 'easy-invoice'),
291 385 __('Preview Invoice', 'easy-invoice'),
292 - 'manage_options',
386 + $this->menuCapability(PagesSlugs::INVOICE_PREVIEW),
293 387 PagesSlugs::INVOICE_PREVIEW,
294 388 array($this, 'displayPreviewPage')
295 389 );
296 390
@@ -297,29 +391,46 @@
297 391 add_submenu_page(
298 392 'easy-invoice-hidden', // Use main menu as parent to avoid title issues
299 393 __('Preview Quote', 'easy-invoice'),
300 394 __('Preview Quote', 'easy-invoice'),
301 - 'manage_options',
395 + $this->menuCapability(PagesSlugs::QUOTE_PREVIEW),
302 396 PagesSlugs::QUOTE_PREVIEW,
303 397 array($this, 'displayMainPage')
304 398 );
305 399
400 + // Reports — addon-gated (Personal tier). Slug registered as a
401 + // HIDDEN submenu so the WP admin sidebar doesn't double the
402 + // in-app sidebar entry. When the reports addon is disabled, the
403 + // slug isn't registered at all, so direct-URL access falls
404 + // through to WP's "no permission" page.
405 + if (\EasyInvoice\Addons\AddonManager::shouldLoad('reports')) {
406 + add_submenu_page(
407 + 'easy-invoice-hidden',
408 + __('Reports', 'easy-invoice'),
409 + __('Reports', 'easy-invoice'),
410 + $this->menuCapability(PagesSlugs::REPORTS),
411 + PagesSlugs::REPORTS,
412 + array($this, 'displayMainPage')
413 + );
414 + }
415 +
416 + // Settings submenu
306 417 add_submenu_page(
307 418 'easy-invoice',
308 - __('Reports', 'easy-invoice'),
309 - __('Reports', 'easy-invoice'),
310 - 'manage_options',
311 - PagesSlugs::REPORTS,
419 + __('Settings', 'easy-invoice'),
420 + __('Settings', 'easy-invoice'),
421 + $this->menuCapability(PagesSlugs::SETTINGS),
422 + PagesSlugs::SETTINGS,
312 423 array($this, 'displayMainPage')
313 424 );
314 425
315 - // Settings submenu
426 + // Addons submenu
316 427 add_submenu_page(
317 428 'easy-invoice',
318 - __('Settings', 'easy-invoice'),
319 - __('Settings', 'easy-invoice'),
320 - 'manage_options',
321 - PagesSlugs::SETTINGS,
429 + __('Addons', 'easy-invoice'),
430 + __('Addons', 'easy-invoice'),
431 + $this->menuCapability(PagesSlugs::ADDONS),
432 + PagesSlugs::ADDONS,
322 433 array($this, 'displayMainPage')
323 434 );
324 435
325 436 // License submenu
@@ -326,9 +437,9 @@
326 437 add_submenu_page(
327 438 'easy-invoice',
328 439 __('License', 'easy-invoice'),
329 440 __('License', 'easy-invoice'),
330 - 'manage_options',
441 + $this->menuCapability(PagesSlugs::LICENSE),
331 442 PagesSlugs::LICENSE,
332 443 array($this, 'displayMainPage')
333 444 );
334 445
@@ -337,9 +448,9 @@
337 448 add_submenu_page(
338 449 'easy-invoice',
339 450 __('Free vs Pro', 'easy-invoice'),
340 451 __('Free vs Pro', 'easy-invoice'),
341 - 'manage_options',
452 + $this->menuCapability('easy-invoice-free-vs-pro'),
342 453 'easy-invoice-free-vs-pro',
343 454 array($this, 'displayMainPage')
344 455 );
345 456 }
@@ -358,17 +469,22 @@
358 469 /**
359 470 * Redirect to community page
360 471 */
361 472 public function redirectToCommunity() {
362 - wp_redirect(esc_url_raw('https://www.facebook.com/groups/mantrabraincommunity'));
473 + wp_safe_redirect(esc_url_raw('https://www.facebook.com/groups/mantrabraincommunity'));
363 474 exit;
364 475 }
365 476
366 477 /**
367 - * Register invoice preview page
478 + * Register invoice preview page.
479 + *
480 + * Placeholder kept as an extension point — the preview-page hook used to
481 + * be wired here but is now routed through the per-document preview
482 + * controller. Left in place so themes/plugins relying on a no-op
483 + * `registerPreviewPage()` call don't fatal.
368 484 */
369 485 public function registerPreviewPage() {
370 - //add_action('admin_action_easy_invoice_preview', array($this, 'displayPreviewPage'));
486 + // No-op: preview rendering is handled by the per-document controller.
371 487 }
372 488
373 489 /**
374 490 * Display main page
@@ -391,8 +507,17 @@
391 507 include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/license-page.php';
392 508 }
393 509
394 510 /**
511 + * Display addons page
512 + */
513 + public function displayAddonsPage() {
514 + if (class_exists('\\EasyInvoice\\Controllers\\AddonsController')) {
515 + (new \EasyInvoice\Controllers\AddonsController())->display();
516 + }
517 + }
518 +
519 + /**
395 520 * Handle main page content based on the current page
396 521 *
397 522 * @param string $page Current page
398 523 */
@@ -427,9 +552,9 @@
427 552 break;
428 553
429 554 case PagesSlugs::PAYMENTS:
430 555 if (isset($_GET['action'])) {
431 - $this->payment_controller->display(['page' => $_GET['action']]);
556 + $this->payment_controller->display(['page' => sanitize_key(wp_unslash($_GET['action']))]); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
432 557 } else {
433 558 $this->payment_controller->display(['page' => PagesSlugs::PAYMENTS]);
434 559 }
435 560 break;
@@ -445,36 +570,43 @@
445 570 case PagesSlugs::CLIENT_EDIT:
446 571 $this->client_controller->display(['page' => PagesSlugs::CLIENT_EDIT]);
447 572 break;
448 573
449 - // Handle Pro features
574 + // Handle Pro features — gated on addon-state. The menu
575 + // registrations above are also addon-gated, so these cases
576 + // should only fire when the addon is enabled. Re-checking
577 + // shouldLoad() here is defensive in case the user hits the
578 + // page slug directly via URL.
450 579 case 'easy-invoice-pro-item-library':
451 - if (easy_invoice_has_pro()) {
452 - // Use the Pro controller's displayMainPage method like other controllers
453 - if (class_exists('\EasyInvoicePro\Controllers\ItemLibraryController')) {
454 - $item_library_controller = new \EasyInvoicePro\Controllers\ItemLibraryController();
455 - $item_library_controller->displayMainPage();
456 - }
580 + if (
581 + easy_invoice_has_pro()
582 + && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')
583 + && class_exists('\EasyInvoicePro\Addons\ItemLibrary\Controllers\ItemLibraryController')
584 + ) {
585 + $item_library_controller = new \EasyInvoicePro\Addons\ItemLibrary\Controllers\ItemLibraryController();
586 + $item_library_controller->displayMainPage();
457 587 }
458 588 break;
459 -
589 +
460 590 case 'easy-invoice-templates':
461 - if (easy_invoice_has_pro()) {
462 - // Use the Pro controller's displayMainPage method like other controllers
463 - if (class_exists('\EasyInvoicePro\Controllers\TemplateBuilderController')) {
464 - $template_builder_controller = new \EasyInvoicePro\Controllers\TemplateBuilderController();
465 - $template_builder_controller->displayMainPage();
466 - }
591 + if (
592 + easy_invoice_has_pro()
593 + && \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')
594 + && class_exists('\EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController')
595 + ) {
596 + $template_builder_controller = new \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController();
597 + $template_builder_controller->displayMainPage();
467 598 }
468 599 break;
469 600
470 601 case 'easy-invoice-template-builder':
471 - if (easy_invoice_has_pro()) {
472 - // Use the Pro controller's displayMainPage method like other controllers
473 - if (class_exists('\EasyInvoicePro\Controllers\TemplateBuilderController')) {
474 - $template_builder_controller = new \EasyInvoicePro\Controllers\TemplateBuilderController();
475 - $template_builder_controller->displayMainPage();
476 - }
602 + if (
603 + easy_invoice_has_pro()
604 + && \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')
605 + && class_exists('\EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController')
606 + ) {
607 + $template_builder_controller = new \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController();
608 + $template_builder_controller->displayMainPage();
477 609 }
478 610 break;
479 611
480 612 case PagesSlugs::CLIENT_VIEW:
@@ -481,9 +613,16 @@
481 613 $this->client_controller->display(['page' => PagesSlugs::CLIENT_VIEW]);
482 614 break;
483 615
484 616 case PagesSlugs::REPORTS:
485 - $this->report_controller->display(['page' => PagesSlugs::REPORTS]);
617 + // Reports is addon-gated. The submenu page slug is only
618 + // registered when shouldLoad('reports') is true, so this
619 + // case ordinarily only fires for active-addon users —
620 + // but recheck defensively in case the page is reached
621 + // via direct URL right after the addon was disabled.
622 + if (\EasyInvoice\Addons\AddonManager::shouldLoad('reports')) {
623 + $this->report_controller->display(['page' => PagesSlugs::REPORTS]);
624 + }
486 625 break;
487 626
488 627 case PagesSlugs::SETTINGS:
489 628 $this->settings_controller->display(['page' => PagesSlugs::SETTINGS]);
@@ -492,8 +631,12 @@
492 631 case PagesSlugs::LICENSE:
493 632 $this->displayLicensePage();
494 633 break;
495 634
635 + case PagesSlugs::ADDONS:
636 + $this->displayAddonsPage();
637 + break;
638 +
496 639 case PagesSlugs::EMAIL_SETTINGS:
497 640 case PagesSlugs::EMAIL_SETTINGS_GENERAL:
498 641 case PagesSlugs::EMAIL_SETTINGS_INVOICE:
499 642 case PagesSlugs::EMAIL_SETTINGS_QUOTE:
@@ -509,8 +652,60 @@
509 652 include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/free-vs-pro-page.php';
510 653 break;
511 654
512 655 default:
656 + // Addon pages render themselves via the
657 + // `easy_invoice_admin_main_content` action at priority 11. The
658 + // dispatcher must NOT fall through to the Dashboard for those
659 + // slugs, otherwise the Dashboard markup gets emitted first and
660 + // the addon panel renders on top of it (two pages stacked).
661 + //
662 + // We treat any slug that's either (a) prefixed with
663 + // `easy-invoice-addon-` or (b) registered in AddonRegistry as
664 + // an addon page — and just `break` to let the addon's own hook
665 + // handle the rendering.
666 + if (strpos($page, 'easy-invoice-addon-') === 0) {
667 + break;
668 + }
669 +
670 + /**
671 + * Slugs whose page draws itself on the same hook at a later
672 + * priority, and which must therefore not get the Dashboard
673 + * rendered underneath them.
674 + *
675 + * Addon pages were special-cased by prefix above, which left
676 + * anything in the free plugin that renders the same way — the
677 + * credit note screen, for one — falling through to the
678 + * Dashboard and stacking two pages on top of each other. A
679 + * filter means the next such page registers itself instead of
680 + * editing this switch and rediscovering the same bug.
681 + *
682 + * @param string[] $slugs Page slugs that render themselves.
683 + */
684 + // "Create New Template" is a deep-link slug with no screen of
685 + // its own; send it to the builder rather than draw the Dashboard.
686 + if ('easy-invoice-templates-new' === $page) {
687 + echo '<script>window.location.replace(' . wp_json_encode(admin_url('admin.php?page=easy-invoice-template-builder&action=new')) . ');</script>';
688 + break;
689 + }
690 + $self_rendering = (array) apply_filters('easy_invoice_self_rendering_pages', [
691 + \EasyInvoice\Controllers\CreditNoteController::PAGE_SLUG,
692 + ]);
693 +
694 + if (in_array($page, $self_rendering, true)) {
695 + break;
696 + }
697 + if (class_exists('\\EasyInvoice\\Addons\\AddonRegistry')) {
698 + foreach (\EasyInvoice\Addons\AddonRegistry::all() as $_addon) {
699 + if (empty($_addon['settings_url'])) continue;
700 + $parsed = wp_parse_url($_addon['settings_url']);
701 + if (empty($parsed['query'])) continue;
702 + parse_str($parsed['query'], $q);
703 + if (($q['page'] ?? '') === $page) {
704 + return; // Addon owns this slug — bail without rendering.
705 + }
706 + }
707 + }
513 708 $this->dashboard_controller->display(['page' => PagesSlugs::DASHBOARD]);
514 709 break;
515 710 }
516 711 }
@@ -543,8 +738,9 @@
543 738 'easy-invoice-settings' => __('Settings', 'easy-invoice'),
544 739 'easy-invoice-email-settings' => __('Email Settings', 'easy-invoice'),
545 740 'easy-invoice-migration' => __('Migration', 'easy-invoice'),
546 741 'easy-invoice-free-vs-pro' => __('Free vs Pro', 'easy-invoice'),
742 + 'easy-invoice-addons' => __('Addons', 'easy-invoice'),
547 743 ];
548 744
549 745 if (isset($page_titles[$current_page])) {
550 746 return $page_titles[$current_page] . ' - ' . get_bloginfo('name');
@@ -554,13 +750,30 @@
554 750 return $title;
555 751 }
556 752
557 753 /**
754 + * Body class for Easy Invoice full-width layout (used by hideAdminUi CSS).
755 + *
756 + * @param string $classes Space-separated classes.
757 + * @return string
758 + */
759 + public function adminBodyClass($classes) {
760 + $screen = get_current_screen();
761 + if (!$screen || !isset($screen->id)) {
762 + return $classes;
763 + }
764 + if (strpos($screen->id, 'easy-invoice') !== false || strpos($screen->id, 'easy-quote') !== false) {
765 + $classes .= ' easy-invoice-admin-body';
766 + }
767 + return $classes;
768 + }
769 +
770 + /**
558 771 * Hide the WordPress admin UI for our custom pages
559 772 */
560 773 public function hideAdminUi() {
561 774 $screen = get_current_screen();
562 - if ($screen && property_exists($screen, 'id') && strpos($screen->id, 'easy-invoice') !== false) {
775 + if ($screen && property_exists($screen, 'id') && (strpos($screen->id, 'easy-invoice') !== false || strpos($screen->id, 'easy-quote') !== false)) {
563 776 ?>
564 777 <style>
565 778 #wpadminbar,
566 779 #adminmenumain,
@@ -573,8 +786,19 @@
573 786 }
574 787 .wrap {
575 788 margin: 0 !important;
576 789 padding: 0 !important;
790 + }
791 + /*
792 + * Admin bar is hidden but body.admin-bar often still gets top padding,
793 + * shifting #wpbody down while the fixed plugin sidebar stays at y=0 — header borders misalign.
794 + */
795 + body.easy-invoice-admin-body.admin-bar {
796 + padding-top: 0 !important;
797 + }
798 + body.easy-invoice-admin-body.admin-bar #wpbody,
799 + body.easy-invoice-admin-body.admin-bar #wpbody-content {
800 + padding-top: 0 !important;
577 801 }
578 802 </style>
579 803 <?php
580 804 }