| @@ -20,11 +20,20 @@ | ||
| 20 | 20 | /** |
| 21 | 21 | * Initialize the controller |
| 22 | 22 | */ |
| 23 | 23 | public function init() { |
| 24 | - // Register AJAX handlers for client management | |
| 25 | - add_action('wp_ajax_easy_invoice_delete_client', array($this, 'deleteClient')); | |
| 26 | - // Other client-related AJAX handlers can be added here | |
| 24 | + // The `easy_invoice_delete_client` AJAX is owned by EasyInvoiceAjax, the | |
| 25 | + // same convention already used for `easy_invoice_search_clients` (see the | |
| 26 | + // tombstone in QuoteController::init). The duplicate registration that used | |
| 27 | + // to live here raced with EasyInvoiceAjax::deleteClient(): both handlers ran | |
| 28 | + // on one request and whichever replied first ended it, so which | |
| 29 | + // implementation won depended on bootstrap order. | |
| 30 | + // | |
| 31 | + // EasyInvoiceAjax::deleteClient() is the one to keep — it checks the | |
| 32 | + // `ei_manage_clients` capability, refuses to delete administrator accounts, | |
| 33 | + // and cleans up the client's invoices, quotes, and payments. The version | |
| 34 | + // below (::deleteClient) did none of that. Keep this tombstone so the | |
| 35 | + // registration doesn't get added back. | |
| 27 | 36 | } |
| 28 | 37 | |
| 29 | 38 | /** |
| 30 | 39 | * Display method implementation |
| @@ -81,83 +90,24 @@ | ||
| 81 | 90 | 'pending_invoices' => 0 |
| 82 | 91 | ]; |
| 83 | 92 | |
| 84 | 93 | // Calculate total revenue from all clients' invoices |
| 94 | + // Revenue and counts from SQL over the persisted totals; this used to load | |
| 95 | + // every invoice on the site as a model to render three header cards. | |
| 85 | 96 | $invoice_repository = InvoiceServiceProvider::getInvoiceRepository(); |
| 86 | - $all_invoices = $invoice_repository->all(); | |
| 97 | + $paid = \EasyInvoice\Services\InvoiceTotalsCache::paidRevenue(); | |
| 87 | 98 | $revenue_by_currency = []; |
| 88 | - | |
| 89 | - // First, get all currencies that exist in the system | |
| 90 | - $all_currencies = []; | |
| 91 | - | |
| 92 | - foreach ($all_invoices as $invoice) { | |
| 93 | - $currency_code = $invoice->getCurrencyCode(); | |
| 94 | - | |
| 95 | - // If currency is empty or "global", get the actual currency that was used | |
| 96 | - if (empty($currency_code) || $currency_code === 'global') { | |
| 97 | - // Get the actual currency from invoice meta | |
| 98 | - $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true); | |
| 99 | - $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD'); | |
| 100 | - } | |
| 101 | - | |
| 102 | - // If currency is still "global", use the global setting | |
| 103 | - if ($currency_code === 'global') { | |
| 104 | - $currency_code = get_option('easy_invoice_currency_code', 'USD'); | |
| 105 | - } | |
| 106 | - | |
| 107 | - // Normalize currency code to uppercase for consistent grouping | |
| 108 | - $currency_code = strtoupper($currency_code); | |
| 109 | - | |
| 110 | - if (!empty($currency_code)) { | |
| 111 | - $all_currencies[$currency_code] = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code); | |
| 112 | - } | |
| 113 | - } | |
| 114 | - | |
| 115 | - // Initialize revenue for all currencies found | |
| 116 | - foreach ($all_currencies as $currency_code => $currency_symbol) { | |
| 99 | + foreach ($paid['revenue'] as $currency_code => $row) { | |
| 117 | 100 | $revenue_by_currency[$currency_code] = [ |
| 118 | - 'amount' => 0, | |
| 119 | - 'symbol' => $currency_symbol, | |
| 120 | - 'currency_code' => $currency_code | |
| 101 | + 'amount' => (float) $row['amount'], | |
| 102 | + 'symbol' => $row['symbol'], | |
| 103 | + 'currency_code' => $currency_code, | |
| 121 | 104 | ]; |
| 122 | 105 | } |
| 106 | + $stats['total_revenue'] = $revenue_by_currency; | |
| 107 | + $stats['total_invoices'] = (int) $invoice_repository->count(); | |
| 108 | + $stats['pending_invoices'] = (int) \EasyInvoice\Services\InvoiceTotalsCache::outstanding()['count']; | |
| 123 | 109 | |
| 124 | - // Now calculate revenue for paid invoices | |
| 125 | - foreach ($all_invoices as $invoice) { | |
| 126 | - if ($invoice->getStatus() === 'paid') { | |
| 127 | - $invoice_total = $invoice->getTotal(); | |
| 128 | - if (!is_numeric($invoice_total)) { | |
| 129 | - continue; | |
| 130 | - } | |
| 131 | - | |
| 132 | - // Get the actual currency from the invoice | |
| 133 | - $currency_code = $invoice->getCurrencyCode(); | |
| 134 | - | |
| 135 | - // If currency is empty or "global", get the actual currency that was used | |
| 136 | - if (empty($currency_code) || $currency_code === 'global') { | |
| 137 | - // Get the actual currency from invoice meta | |
| 138 | - $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true); | |
| 139 | - $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD'); | |
| 140 | - } | |
| 141 | - | |
| 142 | - // If currency is still "global", use the global setting | |
| 143 | - if ($currency_code === 'global') { | |
| 144 | - $currency_code = get_option('easy_invoice_currency_code', 'USD'); | |
| 145 | - } | |
| 146 | - | |
| 147 | - // Normalize currency code to uppercase for consistent grouping | |
| 148 | - $currency_code = strtoupper($currency_code); | |
| 149 | - | |
| 150 | - if (isset($revenue_by_currency[$currency_code])) { | |
| 151 | - $revenue_by_currency[$currency_code]['amount'] += $invoice_total; | |
| 152 | - } | |
| 153 | - } | |
| 154 | - } | |
| 155 | - | |
| 156 | - $stats['total_revenue'] = $revenue_by_currency; | |
| 157 | - $stats['total_invoices'] = count($all_invoices); | |
| 158 | - $stats['pending_invoices'] = count($invoice_repository->findByStatus('pending')); | |
| 159 | - | |
| 160 | 110 | // Prepare template data with pagination |
| 161 | 111 | $template_data = [ |
| 162 | 112 | 'clients' => $clients, |
| 163 | 113 | 'stats' => $stats, |
| @@ -177,17 +127,17 @@ | ||
| 177 | 127 | /** |
| 178 | 128 | * Display client edit page |
| 179 | 129 | */ |
| 180 | 130 | protected function displayClientEditPage() { |
| 181 | - $check = $this->checkCapability(); | |
| 131 | + $check = $this->checkCapability('ei_manage_clients'); | |
| 182 | 132 | if (is_wp_error($check)) { |
| 183 | - wp_die($check->get_error_message()); | |
| 133 | + wp_die(esc_html($check->get_error_message())); | |
| 184 | 134 | } |
| 185 | 135 | |
| 186 | 136 | $client_id = isset($_GET['client_id']) ? intval($_GET['client_id']) : 0; |
| 187 | 137 | |
| 188 | 138 | if ($client_id <= 0) { |
| 189 | - wp_die(__('Invalid client ID', 'easy-invoice')); | |
| 139 | + wp_die(esc_html__('Invalid client ID', 'easy-invoice')); | |
| 190 | 140 | } |
| 191 | 141 | |
| 192 | 142 | // Get client from repository |
| 193 | 143 | $repository = ClientServiceProvider::getClientRepository(); |
| @@ -193,9 +143,9 @@ | ||
| 193 | 143 | $repository = ClientServiceProvider::getClientRepository(); |
| 194 | 144 | $client = $repository->find($client_id); |
| 195 | 145 | |
| 196 | 146 | if (!$client) { |
| 197 | - wp_die(__('Client not found', 'easy-invoice')); | |
| 147 | + wp_die(esc_html__('Client not found', 'easy-invoice')); | |
| 198 | 148 | } |
| 199 | 149 | |
| 200 | 150 | // Display the template |
| 201 | 151 | $this->displayTemplate( |
| @@ -207,17 +157,17 @@ | ||
| 207 | 157 | /** |
| 208 | 158 | * Display client view page |
| 209 | 159 | */ |
| 210 | 160 | protected function displayClientViewPage() { |
| 211 | - $check = $this->checkCapability(); | |
| 161 | + $check = $this->checkCapability('ei_view_clients'); | |
| 212 | 162 | if (is_wp_error($check)) { |
| 213 | - wp_die($check->get_error_message()); | |
| 163 | + wp_die(esc_html($check->get_error_message())); | |
| 214 | 164 | } |
| 215 | 165 | |
| 216 | 166 | $client_id = isset($_GET['client_id']) ? intval($_GET['client_id']) : 0; |
| 217 | 167 | |
| 218 | 168 | if ($client_id <= 0) { |
| 219 | - wp_die(__('Invalid client ID', 'easy-invoice')); | |
| 169 | + wp_die(esc_html__('Invalid client ID', 'easy-invoice')); | |
| 220 | 170 | } |
| 221 | 171 | |
| 222 | 172 | // Get client from repository |
| 223 | 173 | $repository = ClientServiceProvider::getClientRepository(); |
| @@ -223,9 +173,9 @@ | ||
| 223 | 173 | $repository = ClientServiceProvider::getClientRepository(); |
| 224 | 174 | $client = $repository->find($client_id); |
| 225 | 175 | |
| 226 | 176 | if (!$client) { |
| 227 | - wp_die(__('Client not found', 'easy-invoice')); | |
| 177 | + wp_die(esc_html__('Client not found', 'easy-invoice')); | |
| 228 | 178 | } |
| 229 | 179 | |
| 230 | 180 | // Get client invoices |
| 231 | 181 | $invoices = []; |
| @@ -334,9 +284,9 @@ | ||
| 334 | 284 | /** |
| 335 | 285 | * Delete a client |
| 336 | 286 | */ |
| 337 | 287 | public function deleteClient() { |
| 338 | - if (!$this->handleAjaxSecurity($_POST['nonce'])) { | |
| 288 | + if (!$this->handleAjaxSecurity(($_POST['nonce'] ?? ''))) { | |
| 339 | 289 | return; |
| 340 | 290 | } |
| 341 | 291 | |
| 342 | 292 | // Check client ID |