PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
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 2.1.15 2.1.16 2.1.2 All 57 releases
← All changes | includes/Admin/EasyInvoiceAdmin.php +338 -29 2.1.22.4.0 View file →
@@ -71,8 +71,15 @@
71 71 */
72 72 private $payment_controller;
73 73
74 74 /**
75 + * Template Builder controller instance
76 + *
77 + * @var \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController
78 + */
79 + private $template_builder_controller;
80 +
81 + /**
75 82 * Constructor
76 83 */
77 84 public function __construct() {
78 85 $this->invoice_controller = new InvoiceController();
@@ -81,8 +88,21 @@
81 88 $this->settings_controller = new SettingsController();
82 89 $this->report_controller = new ReportController();
83 90 $this->dashboard_controller = new DashboardController();
84 91 $this->payment_controller = new PaymentController();
92 +
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();
104 + }
85 105 }
86 106
87 107 /**
88 108 * Initialize the admin class
@@ -89,8 +109,9 @@
89 109 */
90 110 public function init() {
91 111 // Add menu items
92 112 add_action('admin_menu', array($this, 'registerMenuPages'));
113 + add_action('admin_menu', array($this, 'relaxMenuCapabilities'), 9999);
93 114
94 115 // Register admin assets
95 116 $admin_assets = new AdminAssets();
96 117 $admin_assets->register();
@@ -103,8 +124,11 @@
103 124
104 125 // Hide admin UI
105 126 add_action('admin_head', array($this, 'hideAdminUi'));
106 127
128 + // Body class for layout fixes (admin bar offset, header strip alignment)
129 + add_filter('admin_body_class', array($this, 'adminBodyClass'));
130 +
107 131 // Initialize controllers
108 132 $this->invoice_controller->init();
109 133 $this->quote_controller->init();
110 134 $this->client_controller->init();
@@ -117,8 +141,56 @@
117 141 add_action('easy_invoice_admin_main_content', array($this, 'mainPageContent'));
118 142 }
119 143
120 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 + /**
121 193 * Register admin menu pages
122 194 */
123 195 public function registerMenuPages() {
124 196 // Main menu item
@@ -124,9 +196,9 @@
124 196 // Main menu item
125 197 add_menu_page(
126 198 __('Easy Invoice', 'easy-invoice'),
127 199 __('Easy Invoice', 'easy-invoice'),
128 - 'manage_options',
200 + $this->menuCapability(PagesSlugs::DASHBOARD),
129 201 PagesSlugs::DASHBOARD,
130 202 array($this, 'displayMainPage'),
131 203 'dashicons-media-text',
132 204 25
@@ -136,9 +208,9 @@
136 208 add_submenu_page(
137 209 'easy-invoice',
138 210 __('Dashboard', 'easy-invoice'),
139 211 __('Dashboard', 'easy-invoice'),
140 - 'manage_options',
212 + $this->menuCapability(PagesSlugs::DASHBOARD),
141 213 PagesSlugs::DASHBOARD,
142 214 array($this, 'displayMainPage')
143 215 );
144 216
@@ -146,9 +218,9 @@
146 218 add_submenu_page(
147 219 'easy-invoice',
148 220 __('All Invoices', 'easy-invoice'),
149 221 __('All Invoices', 'easy-invoice'),
150 - 'manage_options',
222 + $this->menuCapability(PagesSlugs::ALL_INVOICES),
151 223 PagesSlugs::ALL_INVOICES,
152 224 array($this, 'displayMainPage')
153 225 );
154 226
@@ -156,9 +228,9 @@
156 228 add_submenu_page(
157 229 'easy-invoice-hidden',
158 230 __('Add New Invoice', 'easy-invoice'),
159 231 __('Add New', 'easy-invoice'),
160 - 'manage_options',
232 + $this->menuCapability(PagesSlugs::INVOICE_NEW),
161 233 PagesSlugs::INVOICE_NEW,
162 234 array($this, 'displayMainPage')
163 235 );
164 236
@@ -166,9 +238,9 @@
166 238 add_submenu_page(
167 239 'easy-invoice',
168 240 __('All Quotes', 'easy-invoice'),
169 241 __('All Quotes', 'easy-invoice'),
170 - 'manage_options',
242 + $this->menuCapability(PagesSlugs::ALL_QUOTES),
171 243 PagesSlugs::ALL_QUOTES,
172 244 array($this, 'displayMainPage')
173 245 );
174 246
@@ -176,9 +248,9 @@
176 248 add_submenu_page(
177 249 'easy-invoice-hidden',
178 250 __('Add New Quote', 'easy-invoice'),
179 251 __('Add New Quote', 'easy-invoice'),
180 - 'manage_options',
252 + $this->menuCapability(PagesSlugs::QUOTE_NEW),
181 253 PagesSlugs::QUOTE_NEW,
182 254 array($this, 'displayMainPage')
183 255 );
184 256
@@ -186,9 +258,9 @@
186 258 add_submenu_page(
187 259 'easy-invoice-hidden',
188 260 __('Payments', 'easy-invoice'),
189 261 __('Payments', 'easy-invoice'),
190 - 'manage_options',
262 + $this->menuCapability(PagesSlugs::PAYMENTS),
191 263 PagesSlugs::PAYMENTS,
192 264 array($this, 'displayMainPage')
193 265 );
194 266
@@ -196,9 +268,9 @@
196 268 add_submenu_page(
197 269 'easy-invoice-hidden',
198 270 __('Add New Payment', 'easy-invoice'),
199 271 __('Add New Payment', 'easy-invoice'),
200 - 'manage_options',
272 + $this->menuCapability(PagesSlugs::PAYMENT_NEW),
201 273 PagesSlugs::PAYMENT_NEW,
202 274 array($this, 'displayMainPage')
203 275 );
204 276
@@ -206,19 +278,77 @@
206 278 add_submenu_page(
207 279 'easy-invoice',
208 280 __('All Clients', 'easy-invoice'),
209 281 __('All Clients', 'easy-invoice'),
210 - 'manage_options',
282 + $this->menuCapability(PagesSlugs::CLIENTS),
211 283 PagesSlugs::CLIENTS,
212 284 array($this, 'displayMainPage')
213 285 );
214 286
287 + // Item Library + Template Builder pages are reached via the
288 + // Easy Invoice in-app sidebar (templates/main-template.php), NOT
289 + // the WP admin submenu. We still register the page slugs so the
290 + // URLs resolve, but we parent them under 'easy-invoice-hidden'
291 + // so they don't render duplicate entries in the WP submenu —
292 + // the WP submenu showed the link AND the in-app sidebar showed
293 + // the same link, giving the user two visible navigation
294 + // entry-points to the same page.
295 + //
296 + // Registration is still gated on the addon being active — if
297 + // the addon is off, the slug isn't registered, so direct-URL
298 + // access returns the standard "you do not have sufficient
299 + // permissions" page.
300 + if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')) {
301 + add_submenu_page(
302 + 'easy-invoice-hidden',
303 + __('Item Library', 'easy-invoice'),
304 + __('Item Library', 'easy-invoice'),
305 + $this->menuCapability('easy-invoice-pro-item-library'),
306 + 'easy-invoice-pro-item-library',
307 + array($this, 'displayMainPage')
308 + );
309 + }
310 +
311 + if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')) {
312 + // Template listing — reachable from the in-app sidebar.
313 + add_submenu_page(
314 + 'easy-invoice-hidden',
315 + __('Template Builder', 'easy-invoice'),
316 + __('Template Builder', 'easy-invoice'),
317 + $this->menuCapability('easy-invoice-templates'),
318 + 'easy-invoice-templates',
319 + array($this, 'displayMainPage')
320 + );
321 +
322 + // Create New Template — used by deep-links from inside the
323 + // builder UI.
324 + add_submenu_page(
325 + 'easy-invoice-hidden',
326 + __('Create New Template', 'easy-invoice'),
327 + __('Create New', 'easy-invoice'),
328 + $this->menuCapability('easy-invoice-templates-new'),
329 + 'easy-invoice-templates-new',
330 + array($this, 'displayMainPage')
331 + );
332 +
333 + // Template Builder canvas — full-screen builder, reached
334 + // from the listing page's Edit / New buttons.
335 + add_submenu_page(
336 + 'easy-invoice-hidden',
337 + __('Template Builder Page', 'easy-invoice'),
338 + __('Template Builder Page', 'easy-invoice'),
339 + $this->menuCapability('easy-invoice-template-builder'),
340 + 'easy-invoice-template-builder',
341 + array($this, 'displayMainPage')
342 + );
343 + }
344 +
215 345 // Hidden submenu pages - not shown in the menu but accessible
216 346 add_submenu_page(
217 347 'easy-invoice-hidden', // Use main menu as parent to avoid title issues
218 348 __('Edit Client', 'easy-invoice'),
219 349 __('Edit Client', 'easy-invoice'),
220 - 'manage_options',
350 + $this->menuCapability(PagesSlugs::CLIENT_EDIT),
221 351 PagesSlugs::CLIENT_EDIT,
222 352 array($this, 'displayMainPage')
223 353 );
224 354
@@ -225,9 +355,9 @@
225 355 add_submenu_page(
226 356 'easy-invoice-hidden', // Use main menu as parent to avoid title issues
227 357 __('View Client', 'easy-invoice'),
228 358 __('View Client', 'easy-invoice'),
229 - 'manage_options',
359 + $this->menuCapability(PagesSlugs::CLIENT_VIEW),
230 360 PagesSlugs::CLIENT_VIEW,
231 361 array($this, 'displayMainPage')
232 362 );
233 363
@@ -234,9 +364,9 @@
234 364 add_submenu_page(
235 365 'easy-invoice-hidden', // Use main menu as parent to avoid title issues
236 366 __('Preview Invoice', 'easy-invoice'),
237 367 __('Preview Invoice', 'easy-invoice'),
238 - 'manage_options',
368 + $this->menuCapability(PagesSlugs::INVOICE_PREVIEW),
239 369 PagesSlugs::INVOICE_PREVIEW,
240 370 array($this, 'displayPreviewPage')
241 371 );
242 372
@@ -243,29 +373,46 @@
243 373 add_submenu_page(
244 374 'easy-invoice-hidden', // Use main menu as parent to avoid title issues
245 375 __('Preview Quote', 'easy-invoice'),
246 376 __('Preview Quote', 'easy-invoice'),
247 - 'manage_options',
377 + $this->menuCapability(PagesSlugs::QUOTE_PREVIEW),
248 378 PagesSlugs::QUOTE_PREVIEW,
249 379 array($this, 'displayMainPage')
250 380 );
251 381
382 + // Reports — addon-gated (Personal tier). Slug registered as a
383 + // HIDDEN submenu so the WP admin sidebar doesn't double the
384 + // in-app sidebar entry. When the reports addon is disabled, the
385 + // slug isn't registered at all, so direct-URL access falls
386 + // through to WP's "no permission" page.
387 + if (\EasyInvoice\Addons\AddonManager::shouldLoad('reports')) {
388 + add_submenu_page(
389 + 'easy-invoice-hidden',
390 + __('Reports', 'easy-invoice'),
391 + __('Reports', 'easy-invoice'),
392 + $this->menuCapability(PagesSlugs::REPORTS),
393 + PagesSlugs::REPORTS,
394 + array($this, 'displayMainPage')
395 + );
396 + }
397 +
398 + // Settings submenu
252 399 add_submenu_page(
253 400 'easy-invoice',
254 - __('Reports', 'easy-invoice'),
255 - __('Reports', 'easy-invoice'),
256 - 'manage_options',
257 - PagesSlugs::REPORTS,
401 + __('Settings', 'easy-invoice'),
402 + __('Settings', 'easy-invoice'),
403 + $this->menuCapability(PagesSlugs::SETTINGS),
404 + PagesSlugs::SETTINGS,
258 405 array($this, 'displayMainPage')
259 406 );
260 407
261 - // Settings submenu
408 + // Addons submenu
262 409 add_submenu_page(
263 410 'easy-invoice',
264 - __('Settings', 'easy-invoice'),
265 - __('Settings', 'easy-invoice'),
266 - 'manage_options',
267 - PagesSlugs::SETTINGS,
411 + __('Addons', 'easy-invoice'),
412 + __('Addons', 'easy-invoice'),
413 + $this->menuCapability(PagesSlugs::ADDONS),
414 + PagesSlugs::ADDONS,
268 415 array($this, 'displayMainPage')
269 416 );
270 417
271 418 // License submenu
@@ -272,9 +419,9 @@
272 419 add_submenu_page(
273 420 'easy-invoice',
274 421 __('License', 'easy-invoice'),
275 422 __('License', 'easy-invoice'),
276 - 'manage_options',
423 + $this->menuCapability(PagesSlugs::LICENSE),
277 424 PagesSlugs::LICENSE,
278 425 array($this, 'displayMainPage')
279 426 );
280 427
@@ -283,20 +430,43 @@
283 430 add_submenu_page(
284 431 'easy-invoice',
285 432 __('Free vs Pro', 'easy-invoice'),
286 433 __('Free vs Pro', 'easy-invoice'),
287 - 'manage_options',
434 + $this->menuCapability('easy-invoice-free-vs-pro'),
288 435 'easy-invoice-free-vs-pro',
289 436 array($this, 'displayMainPage')
290 437 );
291 438 }
439 +
440 + // Join Community submenu
441 + add_submenu_page(
442 + 'easy-invoice-hidden',
443 + __('Join Community', 'easy-invoice'),
444 + __('Join Community', 'easy-invoice'),
445 + 'read',
446 + 'easy-invoice-join-community',
447 + array($this, 'redirectToCommunity')
448 + );
292 449 }
293 450
294 451 /**
295 - * Register invoice preview page
452 + * Redirect to community page
296 453 */
454 + public function redirectToCommunity() {
455 + wp_safe_redirect(esc_url_raw('https://www.facebook.com/groups/mantrabraincommunity'));
456 + exit;
457 + }
458 +
459 + /**
460 + * Register invoice preview page.
461 + *
462 + * Placeholder kept as an extension point — the preview-page hook used to
463 + * be wired here but is now routed through the per-document preview
464 + * controller. Left in place so themes/plugins relying on a no-op
465 + * `registerPreviewPage()` call don't fatal.
466 + */
297 467 public function registerPreviewPage() {
298 - //add_action('admin_action_easy_invoice_preview', array($this, 'displayPreviewPage'));
468 + // No-op: preview rendering is handled by the per-document controller.
299 469 }
300 470
301 471 /**
302 472 * Display main page
@@ -319,8 +489,17 @@
319 489 include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/license-page.php';
320 490 }
321 491
322 492 /**
493 + * Display addons page
494 + */
495 + public function displayAddonsPage() {
496 + if (class_exists('\\EasyInvoice\\Controllers\\AddonsController')) {
497 + (new \EasyInvoice\Controllers\AddonsController())->display();
498 + }
499 + }
500 +
501 + /**
323 502 * Handle main page content based on the current page
324 503 *
325 504 * @param string $page Current page
326 505 */
@@ -355,9 +534,9 @@
355 534 break;
356 535
357 536 case PagesSlugs::PAYMENTS:
358 537 if (isset($_GET['action'])) {
359 - $this->payment_controller->display(['page' => $_GET['action']]);
538 + $this->payment_controller->display(['page' => sanitize_key(wp_unslash($_GET['action']))]); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
360 539 } else {
361 540 $this->payment_controller->display(['page' => PagesSlugs::PAYMENTS]);
362 541 }
363 542 break;
@@ -373,14 +552,59 @@
373 552 case PagesSlugs::CLIENT_EDIT:
374 553 $this->client_controller->display(['page' => PagesSlugs::CLIENT_EDIT]);
375 554 break;
376 555
556 + // Handle Pro features — gated on addon-state. The menu
557 + // registrations above are also addon-gated, so these cases
558 + // should only fire when the addon is enabled. Re-checking
559 + // shouldLoad() here is defensive in case the user hits the
560 + // page slug directly via URL.
561 + case 'easy-invoice-pro-item-library':
562 + if (
563 + easy_invoice_has_pro()
564 + && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')
565 + && class_exists('\EasyInvoicePro\Addons\ItemLibrary\Controllers\ItemLibraryController')
566 + ) {
567 + $item_library_controller = new \EasyInvoicePro\Addons\ItemLibrary\Controllers\ItemLibraryController();
568 + $item_library_controller->displayMainPage();
569 + }
570 + break;
571 +
572 + case 'easy-invoice-templates':
573 + if (
574 + easy_invoice_has_pro()
575 + && \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')
576 + && class_exists('\EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController')
577 + ) {
578 + $template_builder_controller = new \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController();
579 + $template_builder_controller->displayMainPage();
580 + }
581 + break;
582 +
583 + case 'easy-invoice-template-builder':
584 + if (
585 + easy_invoice_has_pro()
586 + && \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')
587 + && class_exists('\EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController')
588 + ) {
589 + $template_builder_controller = new \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController();
590 + $template_builder_controller->displayMainPage();
591 + }
592 + break;
593 +
377 594 case PagesSlugs::CLIENT_VIEW:
378 595 $this->client_controller->display(['page' => PagesSlugs::CLIENT_VIEW]);
379 596 break;
380 597
381 598 case PagesSlugs::REPORTS:
382 - $this->report_controller->display(['page' => PagesSlugs::REPORTS]);
599 + // Reports is addon-gated. The submenu page slug is only
600 + // registered when shouldLoad('reports') is true, so this
601 + // case ordinarily only fires for active-addon users —
602 + // but recheck defensively in case the page is reached
603 + // via direct URL right after the addon was disabled.
604 + if (\EasyInvoice\Addons\AddonManager::shouldLoad('reports')) {
605 + $this->report_controller->display(['page' => PagesSlugs::REPORTS]);
606 + }
383 607 break;
384 608
385 609 case PagesSlugs::SETTINGS:
386 610 $this->settings_controller->display(['page' => PagesSlugs::SETTINGS]);
@@ -389,8 +613,12 @@
389 613 case PagesSlugs::LICENSE:
390 614 $this->displayLicensePage();
391 615 break;
392 616
617 + case PagesSlugs::ADDONS:
618 + $this->displayAddonsPage();
619 + break;
620 +
393 621 case PagesSlugs::EMAIL_SETTINGS:
394 622 case PagesSlugs::EMAIL_SETTINGS_GENERAL:
395 623 case PagesSlugs::EMAIL_SETTINGS_INVOICE:
396 624 case PagesSlugs::EMAIL_SETTINGS_QUOTE:
@@ -406,8 +634,60 @@
406 634 include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/free-vs-pro-page.php';
407 635 break;
408 636
409 637 default:
638 + // Addon pages render themselves via the
639 + // `easy_invoice_admin_main_content` action at priority 11. The
640 + // dispatcher must NOT fall through to the Dashboard for those
641 + // slugs, otherwise the Dashboard markup gets emitted first and
642 + // the addon panel renders on top of it (two pages stacked).
643 + //
644 + // We treat any slug that's either (a) prefixed with
645 + // `easy-invoice-addon-` or (b) registered in AddonRegistry as
646 + // an addon page — and just `break` to let the addon's own hook
647 + // handle the rendering.
648 + if (strpos($page, 'easy-invoice-addon-') === 0) {
649 + break;
650 + }
651 +
652 + /**
653 + * Slugs whose page draws itself on the same hook at a later
654 + * priority, and which must therefore not get the Dashboard
655 + * rendered underneath them.
656 + *
657 + * Addon pages were special-cased by prefix above, which left
658 + * anything in the free plugin that renders the same way — the
659 + * credit note screen, for one — falling through to the
660 + * Dashboard and stacking two pages on top of each other. A
661 + * filter means the next such page registers itself instead of
662 + * editing this switch and rediscovering the same bug.
663 + *
664 + * @param string[] $slugs Page slugs that render themselves.
665 + */
666 + // "Create New Template" is a deep-link slug with no screen of
667 + // its own; send it to the builder rather than draw the Dashboard.
668 + if ('easy-invoice-templates-new' === $page) {
669 + echo '<script>window.location.replace(' . wp_json_encode(admin_url('admin.php?page=easy-invoice-template-builder&action=new')) . ');</script>';
670 + break;
671 + }
672 + $self_rendering = (array) apply_filters('easy_invoice_self_rendering_pages', [
673 + \EasyInvoice\Controllers\CreditNoteController::PAGE_SLUG,
674 + ]);
675 +
676 + if (in_array($page, $self_rendering, true)) {
677 + break;
678 + }
679 + if (class_exists('\\EasyInvoice\\Addons\\AddonRegistry')) {
680 + foreach (\EasyInvoice\Addons\AddonRegistry::all() as $_addon) {
681 + if (empty($_addon['settings_url'])) continue;
682 + $parsed = wp_parse_url($_addon['settings_url']);
683 + if (empty($parsed['query'])) continue;
684 + parse_str($parsed['query'], $q);
685 + if (($q['page'] ?? '') === $page) {
686 + return; // Addon owns this slug — bail without rendering.
687 + }
688 + }
689 + }
410 690 $this->dashboard_controller->display(['page' => PagesSlugs::DASHBOARD]);
411 691 break;
412 692 }
413 693 }
@@ -440,8 +720,9 @@
440 720 'easy-invoice-settings' => __('Settings', 'easy-invoice'),
441 721 'easy-invoice-email-settings' => __('Email Settings', 'easy-invoice'),
442 722 'easy-invoice-migration' => __('Migration', 'easy-invoice'),
443 723 'easy-invoice-free-vs-pro' => __('Free vs Pro', 'easy-invoice'),
724 + 'easy-invoice-addons' => __('Addons', 'easy-invoice'),
444 725 ];
445 726
446 727 if (isset($page_titles[$current_page])) {
447 728 return $page_titles[$current_page] . ' - ' . get_bloginfo('name');
@@ -451,13 +732,30 @@
451 732 return $title;
452 733 }
453 734
454 735 /**
736 + * Body class for Easy Invoice full-width layout (used by hideAdminUi CSS).
737 + *
738 + * @param string $classes Space-separated classes.
739 + * @return string
740 + */
741 + public function adminBodyClass($classes) {
742 + $screen = get_current_screen();
743 + if (!$screen || !isset($screen->id)) {
744 + return $classes;
745 + }
746 + if (strpos($screen->id, 'easy-invoice') !== false || strpos($screen->id, 'easy-quote') !== false) {
747 + $classes .= ' easy-invoice-admin-body';
748 + }
749 + return $classes;
750 + }
751 +
752 + /**
455 753 * Hide the WordPress admin UI for our custom pages
456 754 */
457 755 public function hideAdminUi() {
458 756 $screen = get_current_screen();
459 - if ($screen && property_exists($screen, 'id') && strpos($screen->id, 'easy-invoice') !== false) {
757 + if ($screen && property_exists($screen, 'id') && (strpos($screen->id, 'easy-invoice') !== false || strpos($screen->id, 'easy-quote') !== false)) {
460 758 ?>
461 759 <style>
462 760 #wpadminbar,
463 761 #adminmenumain,
@@ -470,8 +768,19 @@
470 768 }
471 769 .wrap {
472 770 margin: 0 !important;
473 771 padding: 0 !important;
772 + }
773 + /*
774 + * Admin bar is hidden but body.admin-bar often still gets top padding,
775 + * shifting #wpbody down while the fixed plugin sidebar stays at y=0 — header borders misalign.
776 + */
777 + body.easy-invoice-admin-body.admin-bar {
778 + padding-top: 0 !important;
779 + }
780 + body.easy-invoice-admin-body.admin-bar #wpbody,
781 + body.easy-invoice-admin-body.admin-bar #wpbody-content {
782 + padding-top: 0 !important;
474 783 }
475 784 </style>
476 785 <?php
477 786 }