PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.3
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.3
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 | templates/main-template.php +408 -138 2.1.19 → 2.4.3 View file →
@@ -1,5 +1,8 @@
1 1 <?php
2 +if ( ! defined( 'ABSPATH' ) ) {
3 + exit;
4 +}
2 5 $current_page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : 'easy-invoice';
3 6
4 7 // Function to check if current page is a subpage of a main menu
5 8 function is_subpage_of($current_page, $main_page) {
@@ -13,9 +16,9 @@
13 16 function get_menu_class($current_page, $menu_page, $is_active = false) {
14 17 if ($is_active) {
15 18 return 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600';
16 19 }
17 - return 'text-gray-600 hover:bg-gray-50 hover:text-gray-900';
20 + return 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700';
18 21 }
19 22
20 23 // Function to get the active icon class
21 24 function get_icon_class($current_page, $menu_page, $is_active = false) {
@@ -21,9 +24,9 @@
21 24 function get_icon_class($current_page, $menu_page, $is_active = false) {
22 25 if ($is_active) {
23 26 return 'text-indigo-600';
24 27 }
25 - return 'text-gray-400 group-hover:text-gray-500';
28 + return 'text-gray-400 group-hover:text-indigo-500';
26 29 }
27 30
28 31 // Determine which menu should be active
29 32 $is_dashboard_active = $current_page === 'easy-invoice';
@@ -65,28 +68,63 @@
65 68 'easy-invoice-email-settings-payment-reminder',
66 69 'easy-invoice-pro-settings', // Pro settings
67 70 'easy-invoice-pro-translations' // Pro translations
68 71 ]);
72 +// The "Addons" sidebar entry is only active on the Addons grid itself.
73 +// Per-addon settings pages (Time Tracking / Smart Reminders / White-Label /
74 +// Team & Audit / Webhooks) are now top-level sidebar entries — each highlights
75 +// itself; the parent Addons row should NOT also light up.
76 +$is_addons_active = ($current_page === 'easy-invoice-addons');
69 77 ?>
70 78
71 -<!-- Sidebar -->
72 -<div class="fixed inset-y-0 left-0 w-64 bg-white shadow-lg border-r border-gray-200">
73 - <!-- Logo and Back Button -->
74 - <div class="ei-app-sidebar-top flex flex-col items-center justify-center px-4 py-2 border-b border-gray-200 box-border">
75 - <div class="flex items-center space-x-2 mb-1">
76 - <div class="flex items-center justify-center w-8 h-8 bg-indigo-600 rounded">
77 - <!-- Invoice Logo SVG -->
78 - <svg class="h-5 w-5 text-white" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
79 - <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
80 - <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
81 - <path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
82 - <path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
83 - <path d="M10 9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
84 - </svg>
85 - </div>
86 - <span class="text-lg font-semibold text-gray-900"><?php esc_html_e('Easy Invoice', 'easy-invoice'); ?></span>
87 - </div>
88 - <a href="<?php echo admin_url(); ?>" target="_blank" class="inline-flex items-center text-xs text-gray-500 hover:text-gray-700 transition-colors duration-200">
79 +<?php
80 +/*
81 + * The sidebar is 256px wide and fixed. It used to carry no responsive classes at
82 + * all, and the content wrapper below was an unconditional `pl-64` — so on a 390px
83 + * phone the sidebar ate 256px and left ~134px for the actual page. Stat cards
84 + * became unreadable slivers, tables collapsed to a column of checkboxes, and every
85 + * screen overflowed horizontally.
86 + *
87 + * It is now an off-canvas drawer under `lg` (slid out by default, toggled by the
88 + * button below) and pinned open from `lg` up, which is the behaviour it always had
89 + * on desktop. The toggle script near the bottom of this file adds and removes
90 + * `-translate-x-full` on the sidebar to slide it; `lg:translate-x-0` overrides that
91 + * from 1024px up, so the drawer state is irrelevant on desktop.
92 + */
93 +?>
94 +<button type="button" id="ei-sidebar-toggle"
95 + class="lg:hidden fixed top-3 left-3 z-[60] inline-flex items-center justify-center w-11 h-11 rounded-md bg-white border border-gray-300 shadow-sm text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
96 + aria-controls="ei-app-sidebar" aria-expanded="false">
97 + <span class="sr-only"><?php esc_html_e('Toggle Easy Invoice navigation', 'easy-invoice'); ?></span>
98 + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
99 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
100 + </svg>
101 +</button>
102 +
103 +<div id="ei-sidebar-backdrop" class="lg:hidden fixed inset-0 bg-gray-900 bg-opacity-50 z-40 hidden" aria-hidden="true"></div>
104 +
105 +<div id="ei-app-sidebar" class="fixed inset-y-0 left-0 w-64 bg-white shadow-lg border-r border-gray-200 ei-app-sidebar flex flex-col z-50 transform -translate-x-full transition-transform duration-200 ease-in-out lg:translate-x-0">
106 +
107 + <div class="ei-app-sidebar-top flex flex-col items-center justify-center px-4 py-2 border-b border-gray-200 box-border flex-shrink-0">
108 + <?php
109 + /**
110 + * Default logo HTML. Addons (White-Label) can short-circuit this to
111 + * inject a custom image/label without touching this template.
112 + *
113 + * Filter: easy_invoice_admin_logo_html
114 + * Args: ($default_html)
115 + */
116 + $default_logo_html = '<div class="flex items-center space-x-2 mb-1">'
117 + . '<div class="flex items-center justify-center w-8 h-8 rounded ei-app-logo-mark">'
118 + . easy_invoice_logo_mark_svg(32)
119 + . '</div>'
120 + . '<span class="text-lg font-semibold text-gray-900 ei-app-logo-label">' . esc_html__('Easy Invoice', 'easy-invoice') . '</span>'
121 + . '</div>';
122 + // wp_kses_post() drops <svg>; the logo list keeps the mark intact.
123 + echo easy_invoice_kses_logo(apply_filters('easy_invoice_admin_logo_html', $default_logo_html));
124 + ?>
125 + <?php if (apply_filters('easy_invoice_admin_show_back_to_wp', true)): ?>
126 + <a href="<?php echo esc_url(admin_url()); ?>" target="_blank" class="inline-flex items-center text-xs text-gray-500 hover:text-gray-700 transition-colors duration-200">
89 127 <svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
90 128 <path d="M19 12H5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
91 129 <path d="M12 19L5 12L12 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
92 130 </svg>
@@ -91,12 +129,14 @@
91 129 <path d="M12 19L5 12L12 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
92 130 </svg>
93 131 <?php esc_html_e( 'Back to WordPress', 'easy-invoice' ); ?>
94 132 </a>
133 + <?php endif; ?>
95 134
96 - <!-- Version Information -->
135 +
136 + <?php if (apply_filters('easy_invoice_admin_show_version_badges', true)): ?>
97 137 <div class="mt-1 flex flex-wrap items-center justify-center gap-1.5">
98 - <!-- Free Version Badge -->
138 +
99 139 <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700 border border-gray-200">
100 140 <svg class="w-2.5 h-2.5 mr-1 text-gray-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
101 141 <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
102 142 <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -105,26 +145,27 @@
105 145 v<?php echo esc_html(EASY_INVOICE_VERSION); ?>
106 146 </span>
107 147
108 148 <?php if (defined('EASY_INVOICE_PRO_VERSION') && function_exists('is_plugin_active') && is_plugin_active('easy-invoice-pro/easy-invoice-pro.php')): ?>
109 - <!-- Pro Version Badge -->
149 +
110 150 <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700 border border-green-200">
111 151 <svg class="w-2.5 h-2.5 mr-1 text-green-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
112 152 <path d="M9 12L11 14L15 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
113 153 <path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
114 154 </svg>
115 - <?php echo esc_html( sprintf( __( 'Pro v%s', 'easy-invoice' ), EASY_INVOICE_PRO_VERSION ) ); ?>
155 + <?php /* translators: %s: Pro plugin version. */ echo esc_html( sprintf( __( 'Pro v%s', 'easy-invoice' ), EASY_INVOICE_PRO_VERSION ) ); ?>
116 156 </span>
117 157 <?php endif; ?>
118 158 </div>
159 + <?php endif; // easy_invoice_admin_show_version_badges ?>
119 160 </div>
120 161
121 - <!-- Navigation Links -->
122 - <nav class="mt-6 px-3 space-y-2">
123 - <!-- Dashboard -->
124 - <a href="<?php echo admin_url('admin.php?page=easy-invoice'); ?>"
125 - class="<?php echo $is_dashboard_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
126 - <svg class="<?php echo $is_dashboard_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
162 +
163 + <nav class="ei-app-sidebar-nav flex-1 min-h-0 overflow-y-auto mt-6 px-3 pb-4 space-y-2">
164 +
165 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice')); ?>"
166 + class="<?php echo esc_attr($is_dashboard_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
167 + <svg class="<?php echo $is_dashboard_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
127 168 <path d="M3 13H9V19H3V13Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
128 169 <path d="M9 13H15V19H9V13Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
129 170 <path d="M15 13H21V19H15V13Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
130 171 <path d="M3 5H9V11H3V5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -133,48 +174,58 @@
133 174 </svg>
134 175 <?php esc_html_e( 'Dashboard', 'easy-invoice' ); ?>
135 176 </a>
136 177
137 - <!-- Invoices -->
138 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-all'); ?>"
139 - class="<?php echo $is_invoices_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
140 - <svg class="<?php echo $is_invoices_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
178 +
179 + <?php
180 +// A sidebar entry is drawn only when the current user can open the page it
181 +// leads to: administrators can open everything; a team member with an EI role
182 +// (Pro Team Roles) is mapped through the same filter the menu uses.
183 +$ei_can_open = static function ($slug) {
184 + return current_user_can((string) apply_filters('easy_invoice_menu_capability', 'manage_options', $slug));
185 +};
186 +?>
187 +<a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-all')); ?>"
188 + class="<?php echo esc_attr($is_invoices_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
189 + <svg class="<?php echo $is_invoices_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
141 190 <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
142 191 <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
143 - <path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
144 - <path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
145 - <path d="M10 9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
192 + <path d="M12 11V19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
193 + <path d="M14.5 13H10.75a1.75 1.75 0 0 0 0 3.5h2.5a1.75 1.75 0 0 1 0 3.5H9.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
146 194 </svg>
147 195 <?php esc_html_e( 'Invoices', 'easy-invoice' ); ?>
148 196 </a>
149 197
150 - <!-- Quotes -->
151 - <a href="<?php echo admin_url('admin.php?page=easy-quote-all'); ?>"
152 - class="<?php echo $is_quotes_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
153 - <svg class="<?php echo $is_quotes_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
198 +
199 + <?php if ($ei_can_open('easy-quote-all')) : ?>
200 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-quote-all')); ?>"
201 + class="<?php echo esc_attr($is_quotes_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
202 + <svg class="<?php echo $is_quotes_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
154 203 <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
155 204 <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
156 - <path d="M9 9H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
157 - <path d="M9 13H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
158 - <path d="M9 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
205 + <path d="M8.5 15.5L11 18L16 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
159 206 </svg>
160 207 <?php esc_html_e( 'Quotes', 'easy-invoice' ); ?>
161 208 </a>
209 + <?php endif; ?>
162 210
163 - <!-- Payments -->
164 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-payments'); ?>"
165 - class="<?php echo $is_payments_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
166 - <svg class="<?php echo $is_payments_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
211 +
212 + <?php if ($ei_can_open('easy-invoice-payments')) : ?>
213 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-payments')); ?>"
214 + class="<?php echo esc_attr($is_payments_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
215 + <svg class="<?php echo $is_payments_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
167 216 <rect x="1" y="4" width="22" height="16" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
168 217 <line x1="1" y1="10" x2="23" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
169 218 </svg>
170 219 <?php esc_html_e( 'Payments', 'easy-invoice' ); ?>
171 220 </a>
221 + <?php endif; ?>
172 222
173 - <!-- Clients -->
174 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-clients'); ?>"
175 - class="<?php echo $is_clients_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
176 - <svg class="<?php echo $is_clients_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
223 +
224 + <?php if ($ei_can_open('easy-invoice-clients')) : ?>
225 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-clients')); ?>"
226 + class="<?php echo esc_attr($is_clients_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
227 + <svg class="<?php echo $is_clients_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
177 228 <path d="M17 21V19C17 17.9391 16.5786 16.9217 15.8284 16.1716C15.0783 15.4214 14.0609 15 13 15H5C3.93913 15 2.92172 15.4214 2.17157 16.1716C1.42143 16.9217 1 17.9391 1 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
178 229 <circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
179 230 <path d="M23 21V19C23 18.1137 22.6485 17.2628 22.0237 16.6425C21.3989 16.0221 20.5471 15.6722 19.66 15.6722C18.7729 15.6722 17.9211 16.0221 17.2963 16.6425C16.6715 17.2628 16.32 18.1137 16.32 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
180 231 <path d="M16.32 11.6722C18.36 11.6722 20 10.0322 20 7.99219C20 5.95219 18.36 4.31219 16.32 4.31219" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -180,39 +231,34 @@
180 231 <path d="M16.32 11.6722C18.36 11.6722 20 10.0322 20 7.99219C20 5.95219 18.36 4.31219 16.32 4.31219" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
181 232 </svg>
182 233 <?php esc_html_e( 'Clients', 'easy-invoice' ); ?>
183 234 </a>
235 + <?php endif; ?>
184 236
185 - <?php if (easy_invoice_has_pro()): ?>
186 - <!-- Item Library -->
187 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-pro-item-library'); ?>"
188 - class="<?php echo $is_item_library_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
189 - <svg class="<?php echo $is_item_library_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
190 - <path d="M20 7H4C2.9 7 2 7.9 2 9V19C2 20.1 2.9 21 4 21H20C21.1 21 22 20.1 22 19V9C22 7.9 21.1 7 20 7Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
191 - <path d="M16 3H8C7.4 3 7 3.4 7 4V7H17V4C17 3.4 16.6 3 16 3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
192 - <path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
193 - <path d="M8 15H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
194 - </svg>
195 - <?php esc_html_e( 'Item Library', 'easy-invoice' ); ?>
196 - </a>
197 -
198 - <!-- Template Builder -->
199 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-templates'); ?>"
200 - class="<?php echo $is_templates_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
201 - <svg class="<?php echo $is_templates_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
202 - <path d="M4 4h7v2H9.5L7.5 7.5H4V4zm7 4h7v2h2.5L17.5 7.5H11V4zm7 4h7v2h2.5L27.5 7.5H18V4z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
203 - <path d="M4 11h7v2H9.5L7.5 14.5H4V11zm7 0h7v2h2.5L17.5 14.5H11V11zm7 0h7v2h2.5L27.5 14.5H18V11z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
204 - <path d="M4 18h7v2H9.5L7.5 21.5H4V18zm7 0h7v2h2.5L17.5 21.5H11V18zm7 0h7v2h2.5L27.5 21.5H18V18z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
205 - </svg>
206 - <?php esc_html_e( 'Template Builder', 'easy-invoice' ); ?>
207 - </a>
208 - <?php endif; ?>
237 + <?php
238 + // Item Library + Template Builder sidebar entries were here.
239 + // They've been moved BELOW the Addons menu — see the per-addon
240 + // sidebar loop further down in this file (lines ~365). The loop
241 + // automatically renders a sidebar entry for every enabled addon
242 + // whose registry entry has a non-empty settings_url, so the two
243 + // explicit blocks are no longer needed (and were creating
244 + // visual duplication with the loop).
245 + ?>
209 246
210 - <!-- Reports -->
247 + <?php
248 + // Reports — addon-gated (PERSONAL tier).
249 + //
250 + // When the addon is loaded, the per-addon sidebar loop further
251 + // down in this file (after the Addons menu entry) renders the
252 + // Reports link automatically — so this block only handles the
253 + // Free-user upsell state. Showing it twice (here AND via the
254 + // loop) was the cause of the "2 Reports pages" duplicate.
255 + ?>
211 256 <?php if (!easy_invoice_has_pro()): ?>
257 +
212 258 <a href="#" id="reports-menu-link"
213 - class="text-gray-600 hover:bg-gray-50 hover:text-gray-900 group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
214 - <svg class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
259 + class="text-gray-600 hover:bg-indigo-50 hover:text-indigo-700 group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
260 + <svg class="text-gray-400 group-hover:text-indigo-500 mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
215 261 <path d="M18 20V10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
216 262 <path d="M12 20V4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
217 263 <path d="M6 20V14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
218 264 </svg>
@@ -223,31 +269,34 @@
223 269 </svg>
224 270 <?php esc_html_e( 'Pro', 'easy-invoice' ); ?>
225 271 </span>
226 272 </a>
227 - <?php else: ?>
228 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-reports'); ?>"
229 - class="<?php echo $is_reports_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
230 - <svg class="<?php echo $is_reports_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
231 - <path d="M18 20V10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
232 - <path d="M12 20V4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
233 - <path d="M6 20V14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
273 + <?php endif; ?>
274 +
275 +
276 + <?php if ($ei_can_open('easy-invoice-settings')) : ?>
277 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-settings')); ?>"
278 + class="<?php echo esc_attr($is_settings_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
279 + <svg class="<?php echo $is_settings_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
280 + <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
281 + <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
234 282 </svg>
235 - <?php esc_html_e( 'Reports', 'easy-invoice' ); ?>
283 + <?php esc_html_e( 'Settings', 'easy-invoice' ); ?>
236 284 </a>
237 285 <?php endif; ?>
238 286
239 - <!-- Settings -->
240 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-settings'); ?>"
241 - class="<?php echo $is_settings_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
242 - <svg class="<?php echo $is_settings_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
243 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
244 - <path d="M19.4 15A1.65 1.65 0 0 0 18 14A6 6 0 0 0 6 14A1.65 1.65 0 0 0 4.6 15A2 2 0 0 0 6 19H18A2 2 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
287 + <?php $is_import_active = ( 'easy-invoice-import' === $current_page ); ?>
288 + <?php if ($ei_can_open('easy-invoice-import')) : ?>
289 + <a href="<?php echo esc_url( admin_url( 'admin.php?page=easy-invoice-import' ) ); ?>"
290 + class="<?php echo esc_attr( $is_import_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700' ); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
291 + <svg class="<?php echo $is_import_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
292 + <path d="M12 3v12m0 0l-4-4m4 4l4-4M4 17v2a2 2 0 002 2h12a2 2 0 002-2v-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
245 293 </svg>
246 - <?php esc_html_e( 'Settings', 'easy-invoice' ); ?>
294 + <?php esc_html_e( 'Import', 'easy-invoice' ); ?>
247 295 </a>
296 + <?php endif; ?>
248 297
249 - <!-- License -->
298 +
250 299 <?php
251 300 // Check if Pro plugin is active and get license status
252 301 $show_license_badge = false;
253 302 $license_status = 'inactive';
@@ -283,31 +332,36 @@
283 332 $license_status = 'free';
284 333 $license_text = __( 'Free', 'easy-invoice' );
285 334 }
286 335 ?>
287 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-license'); ?>"
288 - class="<?php echo ($current_page === 'easy-invoice-license') ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
289 - <svg class="<?php echo ($current_page === 'easy-invoice-license') ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
290 - <path d="M12 2L13.09 8.26L20 9L13.09 9.74L12 16L10.91 9.74L4 9L10.91 8.26L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
336 + <?php if ($ei_can_open('easy-invoice-license')) : ?>
337 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-license')); ?>"
338 + class="<?php echo esc_attr(($current_page === 'easy-invoice-license') ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
339 +
340 + <svg class="<?php echo ($current_page === 'easy-invoice-license') ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
341 + <circle cx="7.5" cy="15.5" r="5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
342 + <path d="M21 2L11.5 11.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
343 + <path d="M15.5 7.5L18.5 10.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
291 344 </svg>
292 345 <span class="flex-1"><?php esc_html_e( 'License', 'easy-invoice' ); ?></span>
293 346 <?php if ($show_license_badge): ?>
294 - <span class="license-status-badge <?php echo $license_status; ?>">
347 + <span class="license-status-badge <?php echo esc_attr($license_status); ?>">
295 348 <span class="status-dot"></span>
296 349 <?php echo esc_html( $license_text ); ?>
297 350 </span>
298 351 <?php endif; ?>
299 352 </a>
353 + <?php endif; ?>
300 354
301 - <!-- Free vs Pro - only show in free version -->
355 +
302 356 <?php
303 357 $show_free_vs_pro = !easy_invoice_has_pro();
304 358
305 359 if ($show_free_vs_pro):
306 360 ?>
307 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-free-vs-pro'); ?>"
308 - class="<?php echo ($current_page === 'easy-invoice-free-vs-pro') ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
309 - <svg class="<?php echo ($current_page === 'easy-invoice-free-vs-pro') ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
361 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-free-vs-pro')); ?>"
362 + class="<?php echo esc_attr(($current_page === 'easy-invoice-free-vs-pro') ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
363 + <svg class="<?php echo ($current_page === 'easy-invoice-free-vs-pro') ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
310 364 <path d="M3 6L21 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
311 365 <path d="M3 12L21 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
312 366 <path d="M3 18L21 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
313 367 <path d="M9 3L9 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -319,14 +373,76 @@
319 373 </span>
320 374 </a>
321 375 <?php endif; ?>
322 376
323 - <!-- Join Community -->
377 + <?php
378 + // Per-addon sidebar entries. Show every enabled addon that has
379 + // its own dedicated page (Smart Reminders, Time Tracking,
380 + // White Label, etc. — anything whose settings_url is NOT a
381 + // deep-link into the main Settings page). Addons whose settings
382 + // live inside Settings → <section> (Secure Links → Advanced,
383 + // Partial Payments → Invoice, etc.) are intentionally excluded:
384 + // the main Settings page's section nav is already the
385 + // navigation aid for those, and a sidebar entry would be a
386 + // duplicate path to the same place. Users still reach those
387 + // via the "Settings →" link on the addon card.
388 + if (class_exists('\\EasyInvoice\\Addons\\AddonRegistry') && class_exists('\\EasyInvoice\\Addons\\AddonManager')):
389 + foreach (\EasyInvoice\Addons\AddonRegistry::all() as $ei_addon):
390 + if (empty($ei_addon['settings_url'])) continue;
391 + if (!\EasyInvoice\Addons\AddonManager::isEnabled($ei_addon['id'])) continue;
392 + if (!\EasyInvoice\Addons\AddonManager::userCanAccess($ei_addon['id'])) continue;
393 +
394 + // Skip addons whose settings_url deep-links into the
395 + // main Settings page (easy-invoice-settings). Those
396 + // are accessed via the card's "Settings →" link only.
397 + $ei_addon_parsed = wp_parse_url($ei_addon['settings_url']);
398 + $ei_addon_query = [];
399 + if (!empty($ei_addon_parsed['query'])) {
400 + parse_str($ei_addon_parsed['query'], $ei_addon_query);
401 + }
402 + $ei_addon_slug = $ei_addon_query['page'] ?? '';
403 + if ($ei_addon_slug === 'easy-invoice-settings') continue;
404 + if (!$ei_can_open($ei_addon_slug)) continue;
405 +
406 + $ei_addon_active = ($current_page === $ei_addon_slug);
407 + $ei_addon_icon = !empty($ei_addon['icon']) ? $ei_addon['icon'] : 'dashicons-admin-plugins';
408 + $ei_addon_label = !empty($ei_addon['short_name']) ? $ei_addon['short_name'] : $ei_addon['name'];
409 + ?>
410 + <a href="<?php echo esc_url($ei_addon['settings_url']); ?>"
411 + class="<?php echo $ei_addon_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200"
412 + title="<?php echo esc_attr($ei_addon['name']); ?>">
413 + <span class="dashicons <?php echo esc_attr($ei_addon_icon); ?> mr-3 flex-shrink-0 h-5 w-5 <?php echo $ei_addon_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?>" style="font-size:20px;width:20px;height:20px;line-height:20px;"></span>
414 + <span class="flex-1 truncate"><?php echo esc_html($ei_addon_label); ?></span>
415 + </a>
416 + <?php
417 + endforeach;
418 + endif;
419 + ?>
420 +
421 +
422 + <?php if ($ei_can_open('easy-invoice-addons')) : ?>
423 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-addons')); ?>"
424 + class="<?php echo esc_attr($is_addons_active ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'); ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
425 + <svg class="<?php echo $is_addons_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
426 + <rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
427 + <rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
428 + <rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
429 + <path d="M17.5 14V21M14 17.5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
430 + </svg>
431 + <span class="flex-1"><?php esc_html_e('Addons', 'easy-invoice'); ?></span>
432 + <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-700">
433 + <?php esc_html_e('New', 'easy-invoice'); ?>
434 + </span>
435 + </a>
436 + <?php endif; ?>
437 +
438 +
439 + <?php if (apply_filters('easy_invoice_show_upgrade_prompts', true)) : ?>
324 440 <a href="<?php echo esc_url('https://www.facebook.com/groups/mantrabraincommunity'); ?>"
325 441 target="_blank"
326 442 rel="noopener noreferrer"
327 - class="<?php echo ($current_page === 'easy-invoice-join-community') ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
328 - <svg class="<?php echo ($current_page === 'easy-invoice-join-community') ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
443 + class="<?php echo ($current_page === 'easy-invoice-join-community') ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'; ?> group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
444 + <svg class="<?php echo ($current_page === 'easy-invoice-join-community') ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
329 445 <path d="M18 2H15C13.6739 2 12.4021 2.52678 11.4645 3.46447C10.5268 4.40215 10 5.67392 10 7V10H7V14H10V22H14V14H17L18 10H14V7C14 6.73478 14.1054 6.48043 14.2929 6.29289C14.4804 6.10536 14.7348 6 15 6H18V2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
330 446 </svg>
331 447 <span class="flex-1"><?php esc_html_e('Join Community', 'easy-invoice'); ?></span>
332 448 <svg class="flex-shrink-0 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -332,18 +448,45 @@
332 448 <svg class="flex-shrink-0 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
333 449 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
334 450 </svg>
335 451 </a>
452 + <?php endif; ?>
336 453
454 +
455 + <?php
456 + /**
457 + * Filter the sidebar help link. Pro's White Label addon points it at
458 + * the agency's own support page.
459 + *
460 + * @param string $url
461 + */
462 + $ei_help_url = apply_filters('easy_invoice_help_link_url', 'https://easy-invoice.matrixaddons.com/docs/');
463 + ?>
464 + <a href="<?php echo esc_url($ei_help_url); ?>"
465 + target="_blank"
466 + rel="noopener noreferrer"
467 + class="text-gray-600 hover:bg-indigo-50 hover:text-indigo-700 group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200">
468 + <svg class="text-gray-400 group-hover:text-indigo-500 mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
469 + <path d="M4 19.5V4.5C4 3.4 4.9 2.5 6 2.5H19C19.5 2.5 20 3 20 3.5V20.5C20 21 19.5 21.5 19 21.5H6C4.9 21.5 4 20.6 4 19.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
470 + <path d="M8 7H16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
471 + <path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
472 + <path d="M8 15H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
473 + </svg>
474 + <span class="flex-1"><?php esc_html_e('Documentation', 'easy-invoice'); ?></span>
475 + <svg class="flex-shrink-0 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
476 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
477 + </svg>
478 + </a>
479 +
337 480 </nav>
338 481
339 - <!-- User Profile -->
340 - <div class="absolute bottom-0 w-full border-t border-gray-200 p-4">
482 +
483 + <div class="ei-app-sidebar-foot border-t border-gray-200 p-4 flex-shrink-0 bg-white">
341 484 <div class="relative">
342 485 <button type="button" id="user-dropdown-button" class="flex items-center w-full text-left hover:bg-gray-50 rounded-lg p-2 transition-colors duration-200">
343 486 <div class="flex-shrink-0">
344 487 <div class="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center">
345 - <span class="text-gray-600 font-medium text-sm"><?php echo substr(wp_get_current_user()->display_name, 0, 1); ?></span>
488 + <span class="text-gray-600 font-medium text-sm"><?php echo esc_html(substr(wp_get_current_user()->display_name, 0, 1)); ?></span>
346 489 </div>
347 490 </div>
348 491 <div class="ml-3 flex-1">
349 492 <p class="text-sm font-medium text-gray-700"><?php echo esc_html(wp_get_current_user()->display_name); ?></p>
@@ -355,22 +498,22 @@
355 498 </svg>
356 499 </div>
357 500 </button>
358 501
359 - <!-- Dropdown Menu -->
502 +
360 503 <div id="user-dropdown-menu" class="hidden absolute bottom-full left-0 right-0 mb-2 bg-white border border-gray-200 rounded-lg shadow-lg z-50">
361 504 <div class="py-1">
362 - <a href="<?php echo admin_url(); ?>" target="_blank" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200">
505 + <a href="<?php echo esc_url(admin_url()); ?>" target="_blank" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200">
363 506 <svg class="mr-3 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
364 507 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
365 508 </svg>
366 - <?php _e('Back to WordPress', 'easy-invoice'); ?>
509 + <?php esc_html_e('Back to WordPress', 'easy-invoice'); ?>
367 510 </a>
368 - <a href="<?php echo wp_logout_url(); ?>" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200">
511 + <a href="<?php echo esc_url(wp_logout_url()); ?>" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200">
369 512 <svg class="mr-3 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
370 513 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
371 514 </svg>
372 - <?php _e('Logout', 'easy-invoice'); ?>
515 + <?php esc_html_e('Logout', 'easy-invoice'); ?>
373 516 </a>
374 517 </div>
375 518 </div>
376 519 </div>
@@ -376,30 +519,28 @@
376 519 </div>
377 520 </div>
378 521 </div>
379 522
380 -<!-- Mobile menu button -->
381 -<div class="fixed top-0 left-0 z-50 p-4 sm:hidden">
382 - <button type="button" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500" aria-expanded="false" aria-label="<?php echo esc_attr__( 'Open menu', 'easy-invoice' ); ?>">
383 - <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
384 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
385 - </svg>
386 - </button>
387 -</div>
523 +<?php
524 +/*
525 + * The old mobile menu button lived here. Removed in favour of #ei-sidebar-toggle
526 + * at the top of this file, because this one could not work:
527 + *
528 + * - Its handler bound to `document.querySelector('button[aria-expanded="false"]')`,
529 + * i.e. the first such button anywhere in the document — not reliably this one.
530 + * - It toggled a `hidden` class the sidebar never had. The sidebar had no
531 + * responsive rules at all, so it was always open; toggling `hidden` removed it
532 + * outright while the content kept its unconditional `pl-64`, leaving a 256px
533 + * empty gutter.
534 + * - It was `sm:hidden`, so it only appeared below 640px, while the layout
535 + * actually breaks below 1024px.
536 + */
537 +?>
388 538
389 539 <script>
390 540 document.addEventListener('DOMContentLoaded', function() {
391 - const mobileMenuButton = document.querySelector('button[aria-expanded="false"]');
392 - const sidebar = document.querySelector('.fixed.inset-y-0.left-0');
541 + /* Sidebar drawer is handled by the #ei-sidebar-toggle script above. */
393 542
394 - if (mobileMenuButton && sidebar) {
395 - mobileMenuButton.addEventListener('click', function() {
396 - const expanded = this.getAttribute('aria-expanded') === 'true';
397 - this.setAttribute('aria-expanded', !expanded);
398 - sidebar.classList.toggle('hidden');
399 - });
400 - }
401 -
402 543 // Handle Reports menu click for premium feature
403 544 const reportsMenuLink = document.getElementById('reports-menu-link');
404 545 if (reportsMenuLink) {
405 546 reportsMenuLink.addEventListener('click', function(e) {
@@ -457,8 +598,65 @@
457 598 body.easy-invoice-admin-body.admin-bar #wpbody-content {
458 599 padding-top: 0 !important;
459 600 }
460 601
602 +/* Keep any admin notice clear of the fixed sidebar.
603 + *
604 + * WordPress prints `admin_notices` output into #wpbody-content BEFORE this
605 + * template's markup, so a notice sits outside the `.lg:pl-64` content column and
606 + * runs edge to edge underneath the 16rem fixed sidebar — the first words of every
607 + * line get covered. disableAdminNoticesOnEasyInvoicePages() suppresses most
608 + * notices on our screens, but not all of them: WordPress core prints some
609 + * (php_update_notice, update-nag) outside that hook, and any notice we
610 + * deliberately keep would be clipped the same way. Indenting them costs nothing
611 + * when there are none to show. */
612 +@media (min-width: 1024px) {
613 + body.easy-invoice-admin-body #wpbody-content > .notice,
614 + body.easy-invoice-admin-body #wpbody-content > .updated,
615 + body.easy-invoice-admin-body #wpbody-content > .update-nag,
616 + body.easy-invoice-admin-body #wpbody-content > .error {
617 + margin-left: 17rem;
618 + margin-right: 1rem;
619 + }
620 +}
621 +
622 +/* Full-height app shell — works WITH the global
623 + `#wpbody-content { display: flex; flex-direction: column; min-height: 100vh }`
624 + rule defined in easy-invoice.css. Because the parent is a flex
625 + column, the wrapper can't rely on `height: 100%` alone (flex
626 + children stay at content height unless given `flex-grow`). We give
627 + the wrapper `flex: 1` so it expands to fill whatever vertical space
628 + the flex column offers, and a `min-height: 100vh` floor so short
629 + pages still paint the bg-gray-50 all the way down to the viewport
630 + bottom. Removing the previous html → body → wpwrap → wpcontent
631 + chain that wasn't doing anything useful in the flex-column world. */
632 +.easy-invoice-admin {
633 + flex: 1;
634 + min-height: 100vh;
635 +}
636 +
637 +/*
638 + * Below `lg` the sidebar collapses to a drawer and #ei-sidebar-toggle is pinned at
639 + * top-left. Every page header starts hard against the left edge, so without this
640 + * the toggle sat on top of the page title ("Invoices" rendered as "voices") and on
641 + * top of the builders' "Back to invoices" link.
642 + *
643 + * Reserve the toggle's width (44px) plus its offset so headers start clear of it.
644 + * Both header variants are covered: `.ei-app-page-header` is used by the eight
645 + * list/settings screens, `.ei-app-header-sync` by the invoice and quote builders.
646 + */
647 +@media (max-width: 1023px) {
648 + /* !important is needed here, not for weight games: five of these headers
649 + (invoice/quote listings, payments, clients, reports) set `padding-left`
650 + in an inline style attribute, which a stylesheet rule cannot otherwise
651 + beat. Those inline paddings pair with a negative margin to bleed the
652 + header full-width, so they cannot simply be removed. */
653 + .ei-app-page-header,
654 + .ei-app-header-sync {
655 + padding-left: 3.75rem !important;
656 + }
657 +}
658 +
461 659 /* Custom color for active icons */
462 660 .group:hover svg {
463 661 color: rgb(79 70 229) !important;
464 662 }
@@ -471,14 +669,46 @@
471 669 /* Hover state for non-active items */
472 670 .group:hover:not(.bg-indigo-50) svg {
473 671 color: rgb(79 70 229) !important;
474 672 }
673 +
674 +/* Sidebar scrollbar — slim, only visible on hover/scroll. Firefox uses scrollbar-* */
675 +.ei-app-sidebar-nav {
676 + scrollbar-width: thin;
677 + scrollbar-color: rgba(156, 163, 175, 0) transparent;
678 + transition: scrollbar-color 0.2s ease;
679 +}
680 +.ei-app-sidebar-nav:hover {
681 + scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
682 +}
683 +/* WebKit (Chrome / Safari / Edge) */
684 +.ei-app-sidebar-nav::-webkit-scrollbar {
685 + width: 8px;
686 +}
687 +.ei-app-sidebar-nav::-webkit-scrollbar-track {
688 + background: transparent;
689 +}
690 +.ei-app-sidebar-nav::-webkit-scrollbar-thumb {
691 + background-color: transparent;
692 + border-radius: 4px;
693 + border: 2px solid transparent;
694 + background-clip: content-box;
695 + transition: background-color 0.2s ease;
696 +}
697 +.ei-app-sidebar-nav:hover::-webkit-scrollbar-thumb {
698 + background-color: rgba(156, 163, 175, 0.45);
699 +}
700 +.ei-app-sidebar-nav::-webkit-scrollbar-thumb:hover {
701 + background-color: rgba(107, 114, 128, 0.7);
702 +}
475 703 </style>
476 704
477 -<div class="min-h-screen bg-gray-50 easy-invoice-admin">
705 +<div class="min-h-screen bg-white easy-invoice-admin">
478 706 <a href="#easy-invoice-main" class="easy-invoice-skip-link screen-reader-text"><?php esc_html_e( 'Skip to main content', 'easy-invoice' ); ?></a>
479 - <!-- Main Content -->
480 - <div id="easy-invoice-main" class="pl-64" tabindex="-1">
707 +
708 + <?php // pl-64 only from `lg` up — under that the sidebar is an off-canvas drawer
709 + // and reserving 256px of padding left phones with ~134px of usable width. ?>
710 + <div id="easy-invoice-main" class="lg:pl-64" tabindex="-1">
481 711 <?php
482 712
483 713 $page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : 'easy-invoice';
484 714 // Content will be inserted here
@@ -490,11 +720,51 @@
490 720
491 721 ?>
492 722 </div>
493 723
494 - <!-- Add ajaxurl for custom notice JavaScript -->
724 +
495 725 <script>
726 + /* Off-canvas sidebar for viewports below Tailwind's `lg` (1024px).
727 + Kept dependency-free and inline so it works on every Easy Invoice screen
728 + without touching the 13 individual page templates. */
729 + (function () {
730 + var toggle = document.getElementById('ei-sidebar-toggle');
731 + var sidebar = document.getElementById('ei-app-sidebar');
732 + var backdrop = document.getElementById('ei-sidebar-backdrop');
733 + if (!toggle || !sidebar || !backdrop) { return; }
734 +
735 + function setOpen(open) {
736 + sidebar.classList.toggle('-translate-x-full', !open);
737 + backdrop.classList.toggle('hidden', !open);
738 + toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
739 + /* Stop the page scrolling behind the drawer while it is open. */
740 + document.body.style.overflow = open ? 'hidden' : '';
741 + }
742 +
743 + toggle.addEventListener('click', function () {
744 + setOpen(sidebar.classList.contains('-translate-x-full'));
745 + });
746 + backdrop.addEventListener('click', function () { setOpen(false); });
747 +
748 + document.addEventListener('keydown', function (e) {
749 + if (e.key === 'Escape' && !sidebar.classList.contains('-translate-x-full')) {
750 + setOpen(false);
751 + toggle.focus();
752 + }
753 + });
754 +
755 + /* Closing on navigation keeps the drawer from being stuck open after a
756 + click-through, and resizing up to desktop must clear the inline
757 + overflow lock the drawer set. */
758 + sidebar.addEventListener('click', function (e) {
759 + if (e.target.closest('a') && window.matchMedia('(max-width: 1023px)').matches) { setOpen(false); }
760 + });
761 + window.matchMedia('(min-width: 1024px)').addEventListener('change', function (ev) {
762 + if (ev.matches) { setOpen(false); document.body.style.overflow = ''; }
763 + });
764 + })();
765 +
496 766 if (typeof ajaxurl === 'undefined') {
497 - var ajaxurl = '<?php echo admin_url('admin-ajax.php'); ?>';
767 + var ajaxurl = '<?php echo esc_url(admin_url('admin-ajax.php')); ?>';
498 768 }
499 769 </script>
500 770 </div>