invoice_controller = new InvoiceController();
$this->quote_controller = new QuoteController();
$this->client_controller = new ClientController();
$this->settings_controller = new SettingsController();
$this->report_controller = new ReportController();
$this->dashboard_controller = new DashboardController();
$this->payment_controller = new PaymentController();
// Initialize Template Builder controller if Pro is active AND
// the custom_templates addon is enabled. The addon owns this
// feature — see addons/CustomTemplates/addon.php in the Pro
// plugin. Constructing the controller here always-on would
// register its admin_menu hook regardless of addon state.
if (
easy_invoice_has_pro()
&& \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')
&& class_exists('\EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController')
) {
$this->template_builder_controller = new \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController();
}
}
/**
* Initialize the admin class
*/
public function init() {
// Add menu items
add_action('admin_menu', array($this, 'registerMenuPages'));
add_action('admin_menu', array($this, 'relaxMenuCapabilities'), 9999);
// Register admin assets
$admin_assets = new AdminAssets();
$admin_assets->register();
// Register invoice preview page
add_action('admin_init', array($this, 'registerPreviewPage'));
// Ensure proper page titles to avoid WordPress core issues
add_filter('admin_title', array($this, 'setAdminTitle'));
// Hide admin UI
add_action('admin_head', array($this, 'hideAdminUi'));
// Body class for layout fixes (admin bar offset, header strip alignment)
add_filter('admin_body_class', array($this, 'adminBodyClass'));
// Initialize controllers
$this->invoice_controller->init();
$this->quote_controller->init();
$this->client_controller->init();
$this->settings_controller->init();
$this->report_controller->init();
$this->dashboard_controller->init();
$this->payment_controller->init();
// Main content hook
add_action('easy_invoice_admin_main_content', array($this, 'mainPageContent'));
}
/**
* Let an addon lower the capability a menu page demands.
*
* Every page is registered with `manage_options`, which is right for a
* site with one administrator and wrong the moment the Team Roles addon
* hands a colleague an "EI Viewer" role: WordPress refused them every
* screen before the plugin's own capability checks were even consulted.
* add_submenu_page() drops a page outright when the current user lacks
* the capability, so the plugin's own pages pass through menuCapability()
* at registration; this late pass covers pages registered elsewhere.
*/
public function menuCapability($slug) {
/** This filter is documented in relaxMenuCapabilities(). */
return (string) apply_filters('easy_invoice_menu_capability', 'manage_options', (string) $slug);
}
public function relaxMenuCapabilities() {
global $menu, $submenu;
$relax = static function ($cap, $slug) {
if (!is_string($slug) || 0 !== strpos($slug, 'easy-') ) {
return $cap;
}
/**
* Filter the capability required to open an Easy Invoice admin page.
*
* @param string $cap Capability registered for the page.
* @param string $slug Page slug.
*/
return (string) apply_filters('easy_invoice_menu_capability', $cap, $slug);
};
if (is_array($menu)) {
foreach ($menu as $i => $item) {
if (isset($item[1], $item[2])) {
$menu[$i][1] = $relax($item[1], $item[2]);
}
}
}
if (is_array($submenu)) {
foreach ($submenu as $parent => $items) {
foreach ((array) $items as $i => $item) {
if (isset($item[1], $item[2])) {
$submenu[$parent][$i][1] = $relax($item[1], $item[2]);
}
}
}
}
}
/**
* Register admin menu pages
*/
public function registerMenuPages() {
// Main menu item
add_menu_page(
__('Easy Invoice', 'easy-invoice'),
__('Easy Invoice', 'easy-invoice'),
$this->menuCapability(PagesSlugs::DASHBOARD),
PagesSlugs::DASHBOARD,
array($this, 'displayMainPage'),
'dashicons-media-text',
25
);
// Dashboard submenu
add_submenu_page(
'easy-invoice',
__('Dashboard', 'easy-invoice'),
__('Dashboard', 'easy-invoice'),
$this->menuCapability(PagesSlugs::DASHBOARD),
PagesSlugs::DASHBOARD,
array($this, 'displayMainPage')
);
// All invoices submenu
add_submenu_page(
'easy-invoice',
__('All Invoices', 'easy-invoice'),
__('All Invoices', 'easy-invoice'),
$this->menuCapability(PagesSlugs::ALL_INVOICES),
PagesSlugs::ALL_INVOICES,
array($this, 'displayMainPage')
);
// Add new invoice submenu
add_submenu_page(
'easy-invoice-hidden',
__('Add New Invoice', 'easy-invoice'),
__('Add New', 'easy-invoice'),
$this->menuCapability(PagesSlugs::INVOICE_NEW),
PagesSlugs::INVOICE_NEW,
array($this, 'displayMainPage')
);
// All quotes submenu
add_submenu_page(
'easy-invoice',
__('All Quotes', 'easy-invoice'),
__('All Quotes', 'easy-invoice'),
$this->menuCapability(PagesSlugs::ALL_QUOTES),
PagesSlugs::ALL_QUOTES,
array($this, 'displayMainPage')
);
// Add new quote submenu
add_submenu_page(
'easy-invoice-hidden',
__('Add New Quote', 'easy-invoice'),
__('Add New Quote', 'easy-invoice'),
$this->menuCapability(PagesSlugs::QUOTE_NEW),
PagesSlugs::QUOTE_NEW,
array($this, 'displayMainPage')
);
// Payments submenu
add_submenu_page(
'easy-invoice-hidden',
__('Payments', 'easy-invoice'),
__('Payments', 'easy-invoice'),
$this->menuCapability(PagesSlugs::PAYMENTS),
PagesSlugs::PAYMENTS,
array($this, 'displayMainPage')
);
// Add new payment submenu
add_submenu_page(
'easy-invoice-hidden',
__('Add New Payment', 'easy-invoice'),
__('Add New Payment', 'easy-invoice'),
$this->menuCapability(PagesSlugs::PAYMENT_NEW),
PagesSlugs::PAYMENT_NEW,
array($this, 'displayMainPage')
);
// Clients submenu
add_submenu_page(
'easy-invoice',
__('All Clients', 'easy-invoice'),
__('All Clients', 'easy-invoice'),
$this->menuCapability(PagesSlugs::CLIENTS),
PagesSlugs::CLIENTS,
array($this, 'displayMainPage')
);
// Item Library + Template Builder pages are reached via the
// Easy Invoice in-app sidebar (templates/main-template.php), NOT
// the WP admin submenu. We still register the page slugs so the
// URLs resolve, but we parent them under 'easy-invoice-hidden'
// so they don't render duplicate entries in the WP submenu —
// the WP submenu showed the link AND the in-app sidebar showed
// the same link, giving the user two visible navigation
// entry-points to the same page.
//
// Registration is still gated on the addon being active — if
// the addon is off, the slug isn't registered, so direct-URL
// access returns the standard "you do not have sufficient
// permissions" page.
if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')) {
add_submenu_page(
'easy-invoice-hidden',
__('Item Library', 'easy-invoice'),
__('Item Library', 'easy-invoice'),
$this->menuCapability('easy-invoice-pro-item-library'),
'easy-invoice-pro-item-library',
array($this, 'displayMainPage')
);
}
if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')) {
// Template listing — reachable from the in-app sidebar.
add_submenu_page(
'easy-invoice-hidden',
__('Template Builder', 'easy-invoice'),
__('Template Builder', 'easy-invoice'),
$this->menuCapability('easy-invoice-templates'),
'easy-invoice-templates',
array($this, 'displayMainPage')
);
// Create New Template — used by deep-links from inside the
// builder UI.
add_submenu_page(
'easy-invoice-hidden',
__('Create New Template', 'easy-invoice'),
__('Create New', 'easy-invoice'),
$this->menuCapability('easy-invoice-templates-new'),
'easy-invoice-templates-new',
array($this, 'displayMainPage')
);
// Template Builder canvas — full-screen builder, reached
// from the listing page's Edit / New buttons.
add_submenu_page(
'easy-invoice-hidden',
__('Template Builder Page', 'easy-invoice'),
__('Template Builder Page', 'easy-invoice'),
$this->menuCapability('easy-invoice-template-builder'),
'easy-invoice-template-builder',
array($this, 'displayMainPage')
);
}
// Hidden submenu pages - not shown in the menu but accessible
add_submenu_page(
'easy-invoice-hidden', // Use main menu as parent to avoid title issues
__('Edit Client', 'easy-invoice'),
__('Edit Client', 'easy-invoice'),
$this->menuCapability(PagesSlugs::CLIENT_EDIT),
PagesSlugs::CLIENT_EDIT,
array($this, 'displayMainPage')
);
add_submenu_page(
'easy-invoice-hidden', // Use main menu as parent to avoid title issues
__('View Client', 'easy-invoice'),
__('View Client', 'easy-invoice'),
$this->menuCapability(PagesSlugs::CLIENT_VIEW),
PagesSlugs::CLIENT_VIEW,
array($this, 'displayMainPage')
);
add_submenu_page(
'easy-invoice-hidden', // Use main menu as parent to avoid title issues
__('Preview Invoice', 'easy-invoice'),
__('Preview Invoice', 'easy-invoice'),
$this->menuCapability(PagesSlugs::INVOICE_PREVIEW),
PagesSlugs::INVOICE_PREVIEW,
array($this, 'displayPreviewPage')
);
add_submenu_page(
'easy-invoice-hidden', // Use main menu as parent to avoid title issues
__('Preview Quote', 'easy-invoice'),
__('Preview Quote', 'easy-invoice'),
$this->menuCapability(PagesSlugs::QUOTE_PREVIEW),
PagesSlugs::QUOTE_PREVIEW,
array($this, 'displayMainPage')
);
// Reports — addon-gated (Personal tier). Slug registered as a
// HIDDEN submenu so the WP admin sidebar doesn't double the
// in-app sidebar entry. When the reports addon is disabled, the
// slug isn't registered at all, so direct-URL access falls
// through to WP's "no permission" page.
if (\EasyInvoice\Addons\AddonManager::shouldLoad('reports')) {
add_submenu_page(
'easy-invoice-hidden',
__('Reports', 'easy-invoice'),
__('Reports', 'easy-invoice'),
$this->menuCapability(PagesSlugs::REPORTS),
PagesSlugs::REPORTS,
array($this, 'displayMainPage')
);
}
// Settings submenu
add_submenu_page(
'easy-invoice',
__('Settings', 'easy-invoice'),
__('Settings', 'easy-invoice'),
$this->menuCapability(PagesSlugs::SETTINGS),
PagesSlugs::SETTINGS,
array($this, 'displayMainPage')
);
// Addons submenu
add_submenu_page(
'easy-invoice',
__('Addons', 'easy-invoice'),
__('Addons', 'easy-invoice'),
$this->menuCapability(PagesSlugs::ADDONS),
PagesSlugs::ADDONS,
array($this, 'displayMainPage')
);
// License submenu
add_submenu_page(
'easy-invoice',
__('License', 'easy-invoice'),
__('License', 'easy-invoice'),
$this->menuCapability(PagesSlugs::LICENSE),
PagesSlugs::LICENSE,
array($this, 'displayMainPage')
);
// Free vs Pro submenu - only show in free version
if (!easy_invoice_has_pro()) {
add_submenu_page(
'easy-invoice',
__('Free vs Pro', 'easy-invoice'),
__('Free vs Pro', 'easy-invoice'),
$this->menuCapability('easy-invoice-free-vs-pro'),
'easy-invoice-free-vs-pro',
array($this, 'displayMainPage')
);
}
// Join Community submenu
add_submenu_page(
'easy-invoice-hidden',
__('Join Community', 'easy-invoice'),
__('Join Community', 'easy-invoice'),
'read',
'easy-invoice-join-community',
array($this, 'redirectToCommunity')
);
}
/**
* Redirect to community page
*/
public function redirectToCommunity() {
wp_safe_redirect(esc_url_raw('https://www.facebook.com/groups/mantrabraincommunity'));
exit;
}
/**
* Register invoice preview page.
*
* Placeholder kept as an extension point — the preview-page hook used to
* be wired here but is now routed through the per-document preview
* controller. Left in place so themes/plugins relying on a no-op
* `registerPreviewPage()` call don't fatal.
*/
public function registerPreviewPage() {
// No-op: preview rendering is handled by the per-document controller.
}
/**
* Display main page
*/
public function displayMainPage() {
include EASY_INVOICE_PLUGIN_DIR . 'templates/main-template.php';
}
/**
* Display preview page
*/
public function displayPreviewPage() {
$this->invoice_controller->display(['page' => PagesSlugs::INVOICE_PREVIEW]);
}
/**
* Display license page
*/
public function displayLicensePage() {
include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/license-page.php';
}
/**
* Display addons page
*/
public function displayAddonsPage() {
if (class_exists('\\EasyInvoice\\Controllers\\AddonsController')) {
(new \EasyInvoice\Controllers\AddonsController())->display();
}
}
/**
* Handle main page content based on the current page
*
* @param string $page Current page
*/
public function mainPageContent($page) {
switch ($page) {
case PagesSlugs::DASHBOARD:
$this->dashboard_controller->display(['page' => PagesSlugs::DASHBOARD]);
break;
case PagesSlugs::ALL_INVOICES:
$this->invoice_controller->display(['page' => PagesSlugs::ALL_INVOICES]);
break;
case PagesSlugs::INVOICE_NEW:
$this->invoice_controller->display(['page' => PagesSlugs::INVOICE_NEW]);
break;
case PagesSlugs::INVOICE_PREVIEW:
$this->invoice_controller->display(['page' => PagesSlugs::INVOICE_PREVIEW]);
break;
case PagesSlugs::ALL_QUOTES:
$this->quote_controller->display(['page' => PagesSlugs::ALL_QUOTES]);
break;
case PagesSlugs::QUOTE_NEW:
$this->quote_controller->display(['page' => PagesSlugs::QUOTE_NEW]);
break;
case PagesSlugs::QUOTE_PREVIEW:
$this->quote_controller->display(['page' => PagesSlugs::QUOTE_PREVIEW]);
break;
case PagesSlugs::PAYMENTS:
if (isset($_GET['action'])) {
$this->payment_controller->display(['page' => sanitize_key(wp_unslash($_GET['action']))]); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
} else {
$this->payment_controller->display(['page' => PagesSlugs::PAYMENTS]);
}
break;
case PagesSlugs::PAYMENT_NEW:
$this->payment_controller->display(['page' => PagesSlugs::PAYMENT_NEW]);
break;
case PagesSlugs::CLIENTS:
$this->client_controller->display(['page' => PagesSlugs::CLIENTS]);
break;
case PagesSlugs::CLIENT_EDIT:
$this->client_controller->display(['page' => PagesSlugs::CLIENT_EDIT]);
break;
// Handle Pro features — gated on addon-state. The menu
// registrations above are also addon-gated, so these cases
// should only fire when the addon is enabled. Re-checking
// shouldLoad() here is defensive in case the user hits the
// page slug directly via URL.
case 'easy-invoice-pro-item-library':
if (
easy_invoice_has_pro()
&& \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')
&& class_exists('\EasyInvoicePro\Addons\ItemLibrary\Controllers\ItemLibraryController')
) {
$item_library_controller = new \EasyInvoicePro\Addons\ItemLibrary\Controllers\ItemLibraryController();
$item_library_controller->displayMainPage();
}
break;
case 'easy-invoice-templates':
if (
easy_invoice_has_pro()
&& \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')
&& class_exists('\EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController')
) {
$template_builder_controller = new \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController();
$template_builder_controller->displayMainPage();
}
break;
case 'easy-invoice-template-builder':
if (
easy_invoice_has_pro()
&& \EasyInvoice\Addons\AddonManager::shouldLoad('custom_templates')
&& class_exists('\EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController')
) {
$template_builder_controller = new \EasyInvoicePro\Addons\CustomTemplates\Controllers\TemplateBuilderController();
$template_builder_controller->displayMainPage();
}
break;
case PagesSlugs::CLIENT_VIEW:
$this->client_controller->display(['page' => PagesSlugs::CLIENT_VIEW]);
break;
case PagesSlugs::REPORTS:
// Reports is addon-gated. The submenu page slug is only
// registered when shouldLoad('reports') is true, so this
// case ordinarily only fires for active-addon users —
// but recheck defensively in case the page is reached
// via direct URL right after the addon was disabled.
if (\EasyInvoice\Addons\AddonManager::shouldLoad('reports')) {
$this->report_controller->display(['page' => PagesSlugs::REPORTS]);
}
break;
case PagesSlugs::SETTINGS:
$this->settings_controller->display(['page' => PagesSlugs::SETTINGS]);
break;
case PagesSlugs::LICENSE:
$this->displayLicensePage();
break;
case PagesSlugs::ADDONS:
$this->displayAddonsPage();
break;
case PagesSlugs::EMAIL_SETTINGS:
case PagesSlugs::EMAIL_SETTINGS_GENERAL:
case PagesSlugs::EMAIL_SETTINGS_INVOICE:
case PagesSlugs::EMAIL_SETTINGS_QUOTE:
case PagesSlugs::EMAIL_SETTINGS_PAYMENT:
$this->settings_controller->display(['page' => PagesSlugs::SETTINGS, 'subsection' => $page]);
break;
case 'easy-invoice-migration':
include EASY_INVOICE_PLUGIN_DIR . 'includes/Migration/templates/migration-page.php';
break;
case 'easy-invoice-free-vs-pro':
include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/free-vs-pro-page.php';
break;
default:
// Addon pages render themselves via the
// `easy_invoice_admin_main_content` action at priority 11. The
// dispatcher must NOT fall through to the Dashboard for those
// slugs, otherwise the Dashboard markup gets emitted first and
// the addon panel renders on top of it (two pages stacked).
//
// We treat any slug that's either (a) prefixed with
// `easy-invoice-addon-` or (b) registered in AddonRegistry as
// an addon page — and just `break` to let the addon's own hook
// handle the rendering.
if (strpos($page, 'easy-invoice-addon-') === 0) {
break;
}
/**
* Slugs whose page draws itself on the same hook at a later
* priority, and which must therefore not get the Dashboard
* rendered underneath them.
*
* Addon pages were special-cased by prefix above, which left
* anything in the free plugin that renders the same way — the
* credit note screen, for one — falling through to the
* Dashboard and stacking two pages on top of each other. A
* filter means the next such page registers itself instead of
* editing this switch and rediscovering the same bug.
*
* @param string[] $slugs Page slugs that render themselves.
*/
// "Create New Template" is a deep-link slug with no screen of
// its own; send it to the builder rather than draw the Dashboard.
if ('easy-invoice-templates-new' === $page) {
echo '';
break;
}
$self_rendering = (array) apply_filters('easy_invoice_self_rendering_pages', [
\EasyInvoice\Controllers\CreditNoteController::PAGE_SLUG,
]);
if (in_array($page, $self_rendering, true)) {
break;
}
if (class_exists('\\EasyInvoice\\Addons\\AddonRegistry')) {
foreach (\EasyInvoice\Addons\AddonRegistry::all() as $_addon) {
if (empty($_addon['settings_url'])) continue;
$parsed = wp_parse_url($_addon['settings_url']);
if (empty($parsed['query'])) continue;
parse_str($parsed['query'], $q);
if (($q['page'] ?? '') === $page) {
return; // Addon owns this slug — bail without rendering.
}
}
}
$this->dashboard_controller->display(['page' => PagesSlugs::DASHBOARD]);
break;
}
}
/**
* Set proper admin title to avoid WordPress core issues
*
* @param string $title The current admin title
* @return string The modified admin title
*/
public function setAdminTitle($title) {
$current_page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : '';
// Only modify titles for our plugin pages
if (strpos($current_page, 'easy-invoice') === 0 || strpos($current_page, 'easy-quote') === 0) {
$page_titles = [
'easy-invoice' => __('Dashboard', 'easy-invoice'),
'easy-invoice-all' => __('All Invoices', 'easy-invoice'),
'easy-invoice-builder' => __('Add New Invoice', 'easy-invoice'),
'easy-invoice-preview' => __('Preview Invoice', 'easy-invoice'),
'easy-quote-all' => __('All Quotes', 'easy-invoice'),
'easy-invoice-quote-builder' => __('Add New Quote', 'easy-invoice'),
'easy-quote-preview' => __('Preview Quote', 'easy-invoice'),
'easy-invoice-payments' => __('Payments', 'easy-invoice'),
'easy-invoice-payment-new' => __('Add New Payment', 'easy-invoice'),
'easy-invoice-clients' => __('Clients', 'easy-invoice'),
'easy-invoice-client-edit' => __('Edit Client', 'easy-invoice'),
'easy-invoice-client-view' => __('View Client', 'easy-invoice'),
'easy-invoice-reports' => __('Reports', 'easy-invoice'),
'easy-invoice-settings' => __('Settings', 'easy-invoice'),
'easy-invoice-email-settings' => __('Email Settings', 'easy-invoice'),
'easy-invoice-migration' => __('Migration', 'easy-invoice'),
'easy-invoice-free-vs-pro' => __('Free vs Pro', 'easy-invoice'),
'easy-invoice-addons' => __('Addons', 'easy-invoice'),
];
if (isset($page_titles[$current_page])) {
return $page_titles[$current_page] . ' - ' . get_bloginfo('name');
}
}
return $title;
}
/**
* Body class for Easy Invoice full-width layout (used by hideAdminUi CSS).
*
* @param string $classes Space-separated classes.
* @return string
*/
public function adminBodyClass($classes) {
$screen = get_current_screen();
if (!$screen || !isset($screen->id)) {
return $classes;
}
if (strpos($screen->id, 'easy-invoice') !== false || strpos($screen->id, 'easy-quote') !== false) {
$classes .= ' easy-invoice-admin-body';
}
return $classes;
}
/**
* Hide the WordPress admin UI for our custom pages
*/
public function hideAdminUi() {
$screen = get_current_screen();
if ($screen && property_exists($screen, 'id') && (strpos($screen->id, 'easy-invoice') !== false || strpos($screen->id, 'easy-quote') !== false)) {
?>