PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.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 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Controllers / InvoiceController.php

InvoiceController.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.1.2, at includes/Controllers/InvoiceController.php

1,167 lines 45.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Invoice Controller Class
4 *
5 * @package Easy_Invoice
6 * @subpackage Controllers
7 */
8
9 namespace EasyInvoice\Controllers;
10
11 use EasyInvoice\Models\Invoice;
12 use EasyInvoice\Providers\InvoiceServiceProvider;
13 use EasyInvoice\Constants\PagesSlugs;
14 use WP_Query;
15
16 /**
17 * InvoiceController handles all invoice-related functionality
18 */
19 class InvoiceController extends BaseController {
20 /**
21 * Initialize the controller
22 */
23 public function init() {
24 // Allow plugins to extend the controller initialization
25 do_action('easy_invoice_invoice_controller_before_init', $this);
26
27 // Register AJAX endpoints for invoice management
28 add_action('wp_ajax_easy_invoice_trash_invoice', array($this, 'trashInvoice'));
29 add_action('wp_ajax_easy_invoice_restore_invoice', array($this, 'restoreInvoice'));
30 add_action('wp_ajax_easy_invoice_delete_invoice_permanently', array($this, 'deleteInvoicePermanently'));
31 add_action('wp_ajax_easy_invoice_delete_invoice', array($this, 'deleteInvoice')); // Legacy support
32 add_action('wp_ajax_easy_invoice_publish_invoice', array($this, 'publishInvoice'));
33 add_action('wp_ajax_easy_invoice_draft_invoice', array($this, 'draftInvoice'));
34
35 // Handle bulk actions
36 add_action('admin_init', array($this, 'handleBulkActions'));
37
38 // Register additional AJAX handlers
39 $this->registerAjaxHandlers();
40
41 // Add meta box for manual payment verification
42 add_action('add_meta_boxes_easy-invoice', array($this, 'add_manual_payment_meta_box'));
43
44 // AJAX handler for creating a sample invoice
45 add_action('wp_ajax_easy_invoice_create_sample_invoice', array($this, 'ajax_create_sample_invoice'));
46
47 // AJAX handler for creating a new invoice with title
48 add_action('wp_ajax_easy_invoice_create_new_invoice', array($this, 'ajax_create_new_invoice'));
49
50 // Allow plugins to extend the controller initialization
51 do_action('easy_invoice_invoice_controller_after_init', $this);
52 }
53
54 /**
55 * Display method implementation
56 *
57 * @param array $args Display arguments
58 */
59 public function display(array $args = []) {
60 // Allow plugins to modify display arguments
61 $args = apply_filters('easy_invoice_invoice_controller_display_args', $args);
62
63 $page = isset($args['page']) ? $args['page'] : '';
64
65 // Allow plugins to modify the page before processing
66 $page = apply_filters('easy_invoice_invoice_controller_display_page', $page, $args);
67
68 switch ($page) {
69 case PagesSlugs::ALL_INVOICES:
70 $this->displayInvoicesPage();
71 break;
72
73 case PagesSlugs::INVOICE_NEW:
74 // For a new invoice, ensure no ID is passed
75 $_GET['id'] = isset($_GET['id']) ? $_GET['id'] : 0;
76 $this->displayInvoiceBuilderPage();
77 break;
78
79 case PagesSlugs::INVOICE_PREVIEW:
80 $this->displayPreviewPage();
81 break;
82
83 default:
84 $this->displayInvoicesPage();
85 break;
86 }
87
88 // Allow plugins to perform actions after display
89 do_action('easy_invoice_invoice_controller_after_display', $page, $args);
90 }
91
92 /**
93 * Display all invoices page
94 *
95 * Uses WordPress's built-in WP_Query and paginate_links() for optimal performance
96 * with large datasets (10,000+ invoices). The pagination is handled efficiently
97 * by WordPress core functions which are optimized for scalability.
98 */
99 protected function displayInvoicesPage() {
100 // Allow plugins to perform actions before displaying invoices page
101 do_action('easy_invoice_invoice_controller_before_display_invoices_page');
102
103 // Get filter parameters
104 $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
105 $recurring_filter = isset($_GET['recurring']) ? sanitize_text_field($_GET['recurring']) : '';
106 $subscription_filter = isset($_GET['subscription']) ? sanitize_text_field($_GET['subscription']) : '';
107 $search_query = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : '';
108 $current_view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'all';
109 $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
110 $per_page = 20;
111 $offset = ($current_page - 1) * $per_page;
112
113 // Build repository query arguments
114 $args = [];
115
116 // Set post status based on view
117 if ($current_view === 'trash') {
118 $args['post_status'] = 'trash';
119 } elseif ($current_view === 'draft') {
120 $args['post_status'] = 'draft';
121 }
122
123 // Build meta query array
124 $meta_query = [];
125
126 // Add status filter if provided
127 if (!empty($status_filter)) {
128 $meta_query[] = [
129 'key' => '_easy_invoice_status',
130 'value' => $status_filter,
131 'compare' => '='
132 ];
133 }
134
135 // Add recurring filter if provided
136 if (!empty($recurring_filter)) {
137 if ($recurring_filter === 'recurring') {
138 // Show only recurring invoices
139 $meta_query[] = [
140 'key' => '_easy_invoice_recurring_enabled',
141 'value' => '1',
142 'compare' => '='
143 ];
144 } elseif ($recurring_filter === 'non-recurring') {
145 // Show only non-recurring invoices
146 $meta_query[] = [
147 'relation' => 'OR',
148 [
149 'key' => '_easy_invoice_recurring_enabled',
150 'compare' => 'NOT EXISTS'
151 ],
152 [
153 'key' => '_easy_invoice_recurring_enabled',
154 'value' => '0',
155 'compare' => '='
156 ]
157 ];
158 }
159 }
160
161 // Add meta query to args if we have any filters
162 if (!empty($meta_query)) {
163 if (count($meta_query) === 1) {
164 $args['meta_query'] = $meta_query[0];
165 } else {
166 $args['meta_query'] = [
167 'relation' => 'AND',
168 ...$meta_query
169 ];
170 }
171 }
172
173 // Add pagination parameters to args
174 $args['posts_per_page'] = $per_page;
175 $args['offset'] = $offset;
176 $args['orderby'] = 'date';
177 $args['order'] = 'DESC';
178
179 // Allow plugins to modify query arguments
180 $args = apply_filters('easy_invoice_invoice_controller_query_args', $args, $current_view, $status_filter);
181
182 // Get paginated invoices using WordPress query
183 $repository = InvoiceServiceProvider::getInvoiceRepository();
184
185 // Use WordPress WP_Query directly for better pagination handling
186 $query_args = array_merge([
187 'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
188 'post_status' => $args['post_status'] ?? 'publish',
189 'posts_per_page' => $per_page,
190 'paged' => $current_page,
191 'orderby' => 'date',
192 'order' => 'DESC',
193 'no_found_rows' => false, // We need this for pagination
194 'update_post_term_cache' => false, // Disable term cache for better performance
195 'update_post_meta_cache' => false, // Disable meta cache for better performance
196 ], $args);
197
198 // Add search functionality
199 if (!empty($search_query)) {
200 // For search, we'll use a simpler approach that works better with WordPress
201 // First, get all invoices that match the search criteria
202 $search_ids = [];
203
204 // Search in post title and content
205 $title_search = new WP_Query([
206 'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
207 'post_status' => $args['post_status'] ?? 'publish',
208 'posts_per_page' => -1,
209 's' => $search_query
210 ]);
211
212 if ($title_search->have_posts()) {
213 $search_ids = array_merge($search_ids, wp_list_pluck($title_search->posts, 'ID'));
214 }
215
216 // Search in meta fields
217 $meta_search = new WP_Query([
218 'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
219 'post_status' => $args['post_status'] ?? 'publish',
220 'posts_per_page' => -1,
221 'meta_query' => [
222 'relation' => 'OR',
223 [
224 'key' => '_easy_invoice_number',
225 'value' => $search_query,
226 'compare' => 'LIKE'
227 ],
228 [
229 'key' => '_easy_invoice_customer_name',
230 'value' => $search_query,
231 'compare' => 'LIKE'
232 ],
233 [
234 'key' => '_easy_invoice_customer_email',
235 'value' => $search_query,
236 'compare' => 'LIKE'
237 ]
238 ]
239 ]);
240
241 if ($meta_search->have_posts()) {
242 $search_ids = array_merge($search_ids, wp_list_pluck($meta_search->posts, 'ID'));
243 }
244
245 // Remove duplicates
246 $search_ids = array_unique($search_ids);
247
248 if (!empty($search_ids)) {
249 // Use post__in to filter by the found IDs
250 $query_args['post__in'] = $search_ids;
251 } else {
252 // If no results found, set post__in to empty array to show no results
253 $query_args['post__in'] = [0];
254 }
255 }
256
257 // Remove offset as we're using paged
258 unset($query_args['offset']);
259
260 // Allow plugins to modify the final query arguments
261 $query_args = apply_filters('easy_invoice_invoice_controller_final_query_args', $query_args);
262
263 $wp_query = new WP_Query($query_args);
264 $invoices = [];
265
266 if ($wp_query->have_posts()) {
267 foreach ($wp_query->posts as $post) {
268 $invoice = $repository->find($post->ID);
269 if ($invoice) {
270 $invoices[] = $invoice;
271 }
272 }
273 }
274
275 // Allow plugins to modify the invoices array
276 $invoices = apply_filters('easy_invoice_invoice_controller_invoices_list', $invoices, $wp_query);
277
278 // Get pagination info from WordPress query
279 $total_invoices = $wp_query->found_posts;
280 $total_pages = $wp_query->max_num_pages;
281
282 // Get trash count for tab display (without pagination)
283 $trash_args = ['post_status' => 'trash'];
284 $trash_invoices = $repository->all($trash_args);
285 $trash_count = count($trash_invoices);
286
287 // Get draft count for tab display (without pagination)
288 $draft_args = ['post_status' => 'draft'];
289 $draft_invoices = $repository->all($draft_args);
290 $draft_count = count($draft_invoices);
291
292 // Prepare template data
293 $template_data = [
294 'invoices' => $invoices,
295 'current_view' => $current_view,
296 'status_filter' => $status_filter,
297 'recurring_filter' => $recurring_filter,
298 'search_query' => $search_query,
299 'trash_count' => $trash_count,
300 'draft_count' => $draft_count,
301 'repository' => $repository,
302 'current_page' => $current_page,
303 'per_page' => $per_page,
304 'total_invoices' => $total_invoices,
305 'total_pages' => $total_pages,
306 'wp_query' => $wp_query
307 ];
308
309 // Allow plugins to modify template data
310 $template_data = apply_filters('easy_invoice_invoice_controller_template_data', $template_data);
311
312 // Display the template
313 $this->displayTemplate(
314 EASY_INVOICE_PLUGIN_DIR . 'templates/invoices/listing.php',
315 $template_data
316 );
317
318 // Allow plugins to perform actions after displaying invoices page
319 do_action('easy_invoice_invoice_controller_after_display_invoices_page', $template_data);
320 }
321
322 /**
323 * Display invoice builder page
324 */
325 protected function displayInvoiceBuilderPage() {
326 $invoice_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
327 $repository = InvoiceServiceProvider::getInvoiceRepository();
328
329 // Display the template
330 $this->displayTemplate(
331 EASY_INVOICE_PLUGIN_DIR . 'templates/invoices/builder.php',
332 ['invoice_id' => $invoice_id, 'repository' => $repository]
333 );
334 }
335
336 /**
337 * Display invoice preview page
338 */
339 protected function displayPreviewPage() {
340 $this->renderInvoicePreview();
341 }
342
343 /**
344 * Common helper method to render an invoice preview
345 * Used by both preview methods to ensure consistency
346 */
347 private function renderInvoicePreview() {
348 $check = $this->checkCapability();
349 if (is_wp_error($check)) {
350 wp_die($check->get_error_message());
351 }
352
353 $invoice_id = isset($_GET['invoice_id']) ? intval($_GET['invoice_id']) : 0;
354
355 if ($invoice_id <= 0) {
356 wp_die(__('Invalid invoice ID', 'easy-invoice'));
357 }
358
359 // Get invoice from repository
360 $repository = InvoiceServiceProvider::getInvoiceRepository();
361 $invoice = $repository->find($invoice_id);
362
363 if (!$invoice) {
364 wp_die(__('Invalid invoice ID', 'easy-invoice'));
365 }
366
367 // Get common template variables
368 $template_vars = $this->getCommonTemplateVars();
369 $currency_symbol = $template_vars['currency_symbol'];
370
371 // Enqueue preview styles
372 wp_enqueue_style(
373 'easy-invoice-preview',
374 EASY_INVOICE_PLUGIN_URL . 'assets/css/preview.css',
375 array(),
376 EASY_INVOICE_VERSION
377 );
378
379 // Display the template
380 $this->displayTemplate(
381 EASY_INVOICE_PLUGIN_DIR . 'templates/invoices/preview.php',
382 [
383 'invoice' => $invoice,
384 'currency_symbol' => $currency_symbol
385 ]
386 );
387 }
388
389 /**
390 * Trash an invoice (move to trash)
391 */
392 public function trashInvoice() {
393 if (!$this->handleAjaxSecurity($_POST['nonce'])) {
394 return;
395 }
396
397 // Check invoice ID
398 if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
399 wp_send_json_error(array('message' => 'Invalid invoice ID'));
400 }
401
402 $invoice_id = intval($_POST['invoice_id']);
403
404 // Get the invoice object to update status
405 $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();
406 $invoice = $invoice_repository->find($invoice_id);
407 if ($invoice) {
408 // Set status to cancelled before moving to trash
409 $invoice->setStatus('cancelled');
410 $invoice->save();
411 }
412
413 // Move to trash
414 $result = wp_trash_post($invoice_id);
415
416 if ($result) {
417 wp_send_json_success(array('message' => 'Invoice moved to trash'));
418 } else {
419 wp_send_json_error(array('message' => 'Error moving invoice to trash'));
420 }
421 }
422
423 /**
424 * Restore an invoice from trash
425 */
426 public function restoreInvoice() {
427 if (!$this->handleAjaxSecurity($_POST['nonce'])) {
428 return;
429 }
430
431 // Check invoice ID
432 if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
433 wp_send_json_error(array('message' => 'Invalid invoice ID'));
434 }
435
436 $invoice_id = intval($_POST['invoice_id']);
437
438 // Restore from trash
439 $result = wp_untrash_post($invoice_id);
440
441 if ($result) {
442 // WordPress defaults restored posts to 'draft', so we need to explicitly set it to 'publish'
443 wp_update_post(array(
444 'ID' => $invoice_id,
445 'post_status' => 'publish'
446 ));
447
448 // Get the invoice object and set status to available
449 $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();
450 $invoice = $invoice_repository->find($invoice_id);
451 if ($invoice) {
452 $invoice->setStatus('available');
453 $invoice->save();
454 }
455
456 wp_send_json_success(array('message' => 'Invoice restored from trash'));
457 } else {
458 wp_send_json_error(array('message' => 'Error restoring invoice from trash'));
459 }
460 }
461
462 /**
463 * Delete an invoice permanently
464 */
465 public function deleteInvoicePermanently() {
466 if (!$this->handleAjaxSecurity($_POST['nonce'])) {
467 return;
468 }
469
470 // Check invoice ID
471 if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
472 wp_send_json_error(array('message' => 'Invalid invoice ID'));
473 }
474
475 $invoice_id = intval($_POST['invoice_id']);
476
477 // Delete permanently
478 $result = wp_delete_post($invoice_id, true);
479
480 if ($result) {
481 wp_send_json_success(array('message' => 'Invoice deleted permanently'));
482 } else {
483 wp_send_json_error(array('message' => 'Error deleting invoice'));
484 }
485 }
486
487 /**
488 * Legacy delete invoice handler (now redirects to trash)
489 */
490 public function deleteInvoice() {
491 // Redirect to trash function for backward compatibility
492 $this->trashInvoice();
493 }
494
495 /**
496 * Publish an invoice (change status from draft to publish)
497 */
498 public function publishInvoice() {
499 if (!$this->handleAjaxSecurity($_POST['nonce'])) {
500 return;
501 }
502
503 // Check invoice ID
504 if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
505 wp_send_json_error(array('message' => 'Invalid invoice ID'));
506 }
507
508 $invoice_id = intval($_POST['invoice_id']);
509
510 // Update post status to published
511 $result = wp_update_post(array(
512 'ID' => $invoice_id,
513 'post_status' => 'publish'
514 ));
515
516 if ($result) {
517 wp_send_json_success(array('message' => 'Invoice published successfully'));
518 } else {
519 wp_send_json_error(array('message' => 'Error publishing invoice'));
520 }
521 }
522
523 /**
524 * Set an invoice to draft status
525 */
526 public function draftInvoice() {
527 if (!$this->handleAjaxSecurity($_POST['nonce'])) {
528 return;
529 }
530
531 // Check invoice ID
532 if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
533 wp_send_json_error(array('message' => 'Invalid invoice ID'));
534 }
535
536 $invoice_id = intval($_POST['invoice_id']);
537
538 // Update post status to draft
539 $result = wp_update_post(array(
540 'ID' => $invoice_id,
541 'post_status' => 'draft'
542 ));
543
544 if ($result) {
545 wp_send_json_success(array('message' => 'Invoice set to draft successfully'));
546 } else {
547 wp_send_json_error(array('message' => 'Error setting invoice to draft'));
548 }
549 }
550
551 /**
552 * Handle bulk actions
553 */
554 public function handleBulkActions() {
555 // Check if we're processing a bulk action
556 if (!isset($_POST['action']) || $_POST['action'] !== 'easy_invoice_bulk_action') {
557 return;
558 }
559
560 // Check nonce and capability
561 $security_check = $this->securityCheck($_POST['easy_invoice_bulk_nonce'], 'easy_invoice_bulk_action');
562 if (is_wp_error($security_check)) {
563 wp_die($security_check->get_error_message());
564 }
565
566 // Check if we have invoice IDs
567 if (!isset($_POST['invoice_ids']) || !is_array($_POST['invoice_ids']) || empty($_POST['invoice_ids'])) {
568 wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_error=no_selection'));
569 exit;
570 }
571
572 // Get bulk action and invoice IDs
573 $bulk_action = isset($_POST['bulk_action']) ? sanitize_text_field($_POST['bulk_action']) : '';
574 $invoice_ids = array_map('intval', $_POST['invoice_ids']);
575
576 // Process based on action
577 $processed = 0;
578
579 switch ($bulk_action) {
580 case 'trash':
581 foreach ($invoice_ids as $id) {
582 if (wp_trash_post($id)) {
583 $processed++;
584 }
585 }
586 wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_trashed=' . $processed));
587 break;
588
589 case 'restore':
590 foreach ($invoice_ids as $id) {
591 if (wp_untrash_post($id)) {
592 // Also set status to publish (since WordPress sets it to draft by default)
593 wp_update_post(array(
594 'ID' => $id,
595 'post_status' => 'publish'
596 ));
597 $processed++;
598 }
599 }
600 wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_restored=' . $processed));
601 break;
602
603 case 'delete':
604 foreach ($invoice_ids as $id) {
605 if (wp_delete_post($id, true)) {
606 $processed++;
607 }
608 }
609 wp_redirect(admin_url('admin.php?page=easy-invoice-all&view=trash&bulk_deleted=' . $processed));
610 break;
611
612 case 'draft':
613 foreach ($invoice_ids as $id) {
614 // Update post status to draft
615 if (wp_update_post(array(
616 'ID' => $id,
617 'post_status' => 'draft'
618 ))) {
619 $processed++;
620 }
621 }
622 wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_drafted=' . $processed));
623 break;
624
625 case 'publish':
626 foreach ($invoice_ids as $id) {
627 // Update post status to publish
628 if (wp_update_post(array(
629 'ID' => $id,
630 'post_status' => 'publish'
631 ))) {
632 $processed++;
633 }
634 }
635 wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_published=' . $processed));
636 break;
637
638 default:
639 wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_error=invalid_action'));
640 }
641
642 exit;
643 }
644
645 /**
646 * Get stats for dashboard
647 */
648 public function getInvoiceStats() {
649 $repository = InvoiceServiceProvider::getInvoiceRepository();
650 $total_invoices = count($repository->all());
651 $pending_invoices = count($repository->findByStatus('pending'));
652 $paid_invoices = count($repository->findByStatus('paid'));
653
654 // Get total revenue by currency from paid invoices
655 $revenue_by_currency = [];
656 $paid_invoices_list = $repository->findByStatus('paid');
657
658 // First, get all currencies that exist in the system
659 $all_invoices = $repository->all();
660 $all_currencies = [];
661
662 foreach ($all_invoices as $invoice) {
663 $currency_code = $invoice->getCurrencyCode();
664
665 // If currency is empty or "global", get the actual currency that was used
666 if (empty($currency_code) || $currency_code === 'global') {
667 // Get the actual currency from invoice meta
668 $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true);
669 $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
670 }
671
672 // If currency is still "global", use the global setting
673 if ($currency_code === 'global') {
674 $currency_code = get_option('easy_invoice_currency_code', 'USD');
675 }
676
677 // Normalize currency code to uppercase for consistent grouping
678 $currency_code = strtoupper($currency_code);
679
680 if (!empty($currency_code)) {
681 $all_currencies[$currency_code] = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
682 }
683 }
684
685 // Initialize revenue for all currencies found
686 foreach ($all_currencies as $currency_code => $currency_symbol) {
687 $revenue_by_currency[$currency_code] = [
688 'amount' => 0,
689 'symbol' => $currency_symbol
690 ];
691 }
692
693 // Now calculate revenue for paid invoices
694 foreach ($paid_invoices_list as $invoice) {
695 $invoice_total = $invoice->getTotal();
696 if (!is_numeric($invoice_total)) {
697 continue;
698 }
699
700 // Get the actual currency from the invoice
701 $currency_code = $invoice->getCurrencyCode();
702
703 // If currency is empty or "global", get the actual currency that was used
704 if (empty($currency_code) || $currency_code === 'global') {
705 // Get the actual currency from invoice meta
706 $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true);
707 $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
708 }
709
710 // If currency is still "global", use the global setting
711 if ($currency_code === 'global') {
712 $currency_code = get_option('easy_invoice_currency_code', 'USD');
713 }
714
715 // Normalize currency code to uppercase for consistent grouping
716 $currency_code = strtoupper($currency_code);
717
718 if (isset($revenue_by_currency[$currency_code])) {
719 $revenue_by_currency[$currency_code]['amount'] += $invoice_total;
720 }
721 }
722
723 // Calculate total value from ALL invoices (not just paid ones)
724 $total_value_by_currency = [];
725
726 // Initialize total value for all currencies found
727 foreach ($all_currencies as $currency_code => $currency_symbol) {
728 $total_value_by_currency[$currency_code] = [
729 'amount' => 0,
730 'invoices' => 0,
731 'invoice_object' => null // Keep reference for formatting
732 ];
733 }
734
735 // Calculate total value from all invoices
736 foreach ($all_invoices as $invoice) {
737 $invoice_total = $invoice->getTotal();
738 if (!is_numeric($invoice_total)) {
739 continue;
740 }
741
742 // Get the actual currency from the invoice
743 $currency_code = $invoice->getCurrencyCode();
744
745 // If currency is empty or "global", get the actual currency that was used
746 if (empty($currency_code) || $currency_code === 'global') {
747 // Get the actual currency from invoice meta
748 $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true);
749 $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
750 }
751
752 // If currency is still "global", use the global setting
753 if ($currency_code === 'global') {
754 $currency_code = get_option('easy_invoice_currency_code', 'USD');
755 }
756
757 // Normalize currency code to uppercase for consistent grouping
758 $currency_code = strtoupper($currency_code);
759
760 if (isset($total_value_by_currency[$currency_code])) {
761 $total_value_by_currency[$currency_code]['amount'] += $invoice_total;
762 $total_value_by_currency[$currency_code]['invoices']++;
763 // Keep reference to first invoice for formatting
764 if ($total_value_by_currency[$currency_code]['invoice_object'] === null) {
765 $total_value_by_currency[$currency_code]['invoice_object'] = $invoice;
766 }
767 }
768 }
769
770 return [
771 'total_invoices' => $total_invoices,
772 'pending_invoices' => $pending_invoices,
773 'paid_invoices' => $paid_invoices,
774 'total_revenue' => $revenue_by_currency,
775 'total_value' => $total_value_by_currency
776 ];
777 }
778
779 /**
780 * Register additional AJAX handlers
781 */
782 public function registerAjaxHandlers() {
783 add_action('wp_ajax_easy_invoice_load_template', array($this, 'handleLoadTemplate'));
784 add_action('wp_ajax_easy_invoice_create_new_invoice', array($this, 'ajax_create_new_invoice'));
785 add_action('wp_ajax_easy_invoice_search_clients', array($this, 'handleSearchClients'));
786 }
787
788 /**
789 * Handle AJAX request to load invoice template
790 */
791 public function handleLoadTemplate() {
792 // Verify nonce
793 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'easy_invoice_nonce')) {
794 wp_send_json_error(array('message' => __('Security check failed', 'easy-invoice')));
795 }
796
797 // Get template name
798 $template = isset($_POST['template']) ? sanitize_text_field($_POST['template']) : 'standard';
799 $invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;
800
801 // For new invoices (no ID), just return the template without invoice data
802 if ($invoice_id === 0) {
803 // Get template file path
804 $template_file = EASY_INVOICE_PLUGIN_DIR . 'templates/invoice-templates/' . $template . '.php';
805
806 if (!file_exists($template_file)) {
807 $template_file = EASY_INVOICE_PLUGIN_DIR . 'templates/invoice-templates/standard.php';
808 }
809
810 // Start output buffering
811 ob_start();
812
813 // Set up empty variables for new invoices
814 $invoice = null;
815 $formatter = null;
816
817
818 include_once $template_file;
819 $html = ob_get_clean();
820
821 // Send response
822 wp_send_json_success(array('html' => $html));
823 return;
824 }
825
826 // Get invoice data for existing invoices
827 $repository = InvoiceServiceProvider::getInvoiceRepository();
828 $invoice = $repository->find($invoice_id);
829
830 if (!$invoice) {
831 wp_send_json_error(array('message' => __('Invoice not found', 'easy-invoice')));
832 }
833
834 // Initialize formatter for currency formatting
835 $formatter = new \EasyInvoice\Helpers\InvoiceFormatter($invoice);
836
837 // Get template file path
838 $template_file = EASY_INVOICE_PLUGIN_DIR . 'templates/invoice-templates/' . $template . '.php';
839
840 if (!file_exists($template_file)) {
841 $template_file = EASY_INVOICE_PLUGIN_DIR . 'templates/invoice-templates/standard.php';
842 }
843
844 // Start output buffering
845 ob_start();
846 include_once $template_file;
847 $html = ob_get_clean();
848
849 // Send response
850 wp_send_json_success(array('html' => $html));
851 }
852
853 /**
854 * Add meta box for manual payment verification to the invoice edit screen.
855 */
856 public function add_manual_payment_meta_box() {
857 add_meta_box(
858 'easy_invoice_manual_payment_verification',
859 __('Manual Payment Verification', 'easy-invoice'),
860 array($this, 'render_manual_payment_meta_box'),
861 'easy-invoice', // Post type
862 'side', // Context
863 'high' // Priority
864 );
865 }
866
867 /**
868 * Render the manual payment verification meta box.
869 *
870 * @param \WP_Post $post The current post object.
871 */
872 public function render_manual_payment_meta_box(\WP_Post $post) {
873 $payment_status = get_post_meta($post->ID, '_payment_status', true);
874 $payment_method = get_post_meta($post->ID, '_payment_method', true);
875
876 if (!in_array($payment_status, ['pending-bank', 'pending-cheque'])) {
877 echo '<p>' . __('This invoice is not pending manual payment verification.', 'easy-invoice') . '</p>';
878 return;
879 }
880
881 wp_nonce_field('easy_invoice_mark_paid_' . $post->ID, 'easy_invoice_mark_paid_nonce');
882
883 echo '<h4>' . __('Submitted Payment Proof', 'easy-invoice') . '</h4>';
884
885 if ($payment_method === 'bank') {
886 $transaction_id = get_post_meta($post->ID, '_bank_transaction_id', true);
887 $notes = get_post_meta($post->ID, '_bank_payment_notes', true);
888 $proof_url = get_post_meta($post->ID, '_bank_payment_proof', true);
889
890 echo '<p><strong>' . __('Transaction ID:', 'easy-invoice') . '</strong> ' . esc_html($transaction_id) . '</p>';
891 if ($notes) {
892 echo '<p><strong>' . __('Notes:', 'easy-invoice') . '</strong></p>';
893 echo '<div style="white-space: pre-wrap; background: #f9f9f9; padding: 5px; border: 1px solid #eee;">' . esc_html($notes) . '</div>';
894 }
895 if ($proof_url) {
896 echo '<p><strong>' . __('Proof Document:', 'easy-invoice') . '</strong> <a href="' . esc_url($proof_url) . '" target="_blank" rel="noopener noreferrer">' . __('View Proof', 'easy-invoice') . '</a></p>';
897 }
898 } elseif ($payment_method === 'cheque') {
899 $cheque_number = get_post_meta($post->ID, '_cheque_number', true);
900 $bank_name = get_post_meta($post->ID, '_cheque_bank_name', true);
901 $cheque_date = get_post_meta($post->ID, '_cheque_date', true);
902 $notes = get_post_meta($post->ID, '_cheque_notes', true);
903 $image_url = get_post_meta($post->ID, '_cheque_image', true);
904
905 echo '<p><strong>' . __('Cheque Number:', 'easy-invoice') . '</strong> ' . esc_html($cheque_number) . '</p>';
906 if ($bank_name) echo '<p><strong>' . __('Bank Name:', 'easy-invoice') . '</strong> ' . esc_html($bank_name) . '</p>';
907 if ($cheque_date) echo '<p><strong>' . __('Cheque Date:', 'easy-invoice') . '</strong> ' . esc_html($cheque_date) . '</p>';
908 if ($notes) {
909 echo '<p><strong>' . __('Notes:', 'easy-invoice') . '</strong></p>';
910 echo '<div style="white-space: pre-wrap; background: #f9f9f9; padding: 5px; border: 1px solid #eee;">' . esc_html($notes) . '</div>';
911 }
912 if ($image_url) {
913 echo '<p><strong>' . __('Cheque Image:', 'easy-invoice') . '</strong> <a href="' . esc_url($image_url) . '" target="_blank" rel="noopener noreferrer">' . __('View Image', 'easy-invoice') . '</a></p>';
914 }
915 }
916
917 echo '<p style="margin-top: 15px;">';
918 echo '<button type="button" id="easy-invoice-mark-paid-btn" class="button button-primary" data-invoice-id="' . esc_attr($post->ID) . '">' . __('Mark as Paid', 'easy-invoice') . '</button>';
919 echo '</p>';
920 echo '<div id="easy-invoice-mark-paid-message" style="margin-top:10px;"></div>';
921
922 // Add a script for the AJAX call
923 ?>
924 <script type="text/javascript">
925 jQuery(document).ready(function($) {
926 $('#easy-invoice-mark-paid-btn').on('click', function() {
927 var invoiceId = $(this).data('invoice-id');
928 var nonce = $('#easy_invoice_mark_paid_nonce').val();
929 var button = $(this);
930 var messageDiv = $('#easy-invoice-mark-paid-message');
931
932 button.prop('disabled', true);
933 messageDiv.html('Processing...');
934
935 $.ajax({
936 url: ajaxurl, // WordPress AJAX URL
937 type: 'POST',
938 data: {
939 action: 'easy_invoice_mark_paid',
940 invoice_id: invoiceId,
941 nonce: nonce
942 },
943 success: function(response) {
944 if (response.success) {
945 messageDiv.css('color', 'green').html(response.data.message);
946 button.hide();
947 // Optionally, reload the page or update UI elements to reflect paid status
948 // window.location.reload();
949 } else {
950 messageDiv.css('color', 'red').html(response.data.message);
951 button.prop('disabled', false);
952 }
953 },
954 error: function() {
955 messageDiv.css('color', 'red').html('<?php echo esc_js(__("An error occurred. Please try again.", "easy-invoice")); ?>');
956 button.prop('disabled', false);
957 }
958 });
959 });
960 });
961 </script>
962 <?php
963 }
964
965 /**
966 * AJAX handler to create a sample invoice.
967 */
968 public function ajax_create_sample_invoice() {
969 // Security check: verify nonce
970 check_ajax_referer('easy_invoice_admin_nonce', 'nonce');
971
972 // Security check: verify user capabilities
973 if (!current_user_can('edit_posts')) { // Or a more specific capability for your CPT
974 wp_send_json_error([
975 'message' => __('You do not have permission to create invoices.', 'easy-invoice')
976 ], 403);
977 return;
978 }
979
980 try {
981 $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();
982
983 // Sample Invoice Data
984 $sample_invoice_data = [
985 'post_title' => 'Sample Invoice - ' . date('Y-m-d H:i'),
986 'post_status' => 'draft', // Or 'publish' if you want it live immediately
987 // Add other WP_Post fields as needed (e.g., post_author)
988 ];
989
990 // Sample Meta Data
991 $sample_meta_data = [
992 '_easy_invoice_number' => 'SAMPLE-' . time(),
993 '_easy_invoice_issue_date' => date('Y-m-d'),
994 '_easy_invoice_due_date' => date('Y-m-d', strtotime('+15 days')),
995 '_easy_invoice_status' => 'draft',
996 '_easy_invoice_customer_name' => 'John Doe (Sample Client)',
997 '_easy_invoice_customer_email' => 'customer@example.com',
998 '_easy_invoice_customer_address' => "123 Sample Street\nSampleville, ST 12345",
999 'currency_code' => 'USD',
1000 'currency_position' => 'before',
1001 // Add other meta keys as needed
1002 ];
1003
1004 // Sample Line Items
1005 $sample_items = [];
1006 for ($i = 1; $i <= 3; $i++) {
1007 $sample_items[] = [
1008 'name' => 'Sample Service ' . $i,
1009 'description' => 'Detailed description of sample service ' . $i . '.',
1010 'quantity' => rand(1, 5),
1011 'price' => rand(50, 200) * 1.00,
1012 // 'taxable' => true/false (optional)
1013 ];
1014 }
1015 $sample_meta_data['_easy_invoice_items'] = $sample_items;
1016
1017 // Create the invoice post
1018 $invoice_id = wp_insert_post($sample_invoice_data, true); // true for WP_Error on failure
1019
1020 if (is_wp_error($invoice_id)) {
1021 throw new \Exception('Failed to create invoice post: ' . $invoice_id->get_error_message());
1022 }
1023
1024 // Set invoice meta data
1025 foreach ($sample_meta_data as $key => $value) {
1026 update_post_meta($invoice_id, $key, $value);
1027 }
1028
1029 // Recalculate totals if your Invoice model or repository has a method for it
1030 // For example, if you have $invoice->calculateTotals()->save(); or similar.
1031 // This step is crucial if subtotal, tax, total are not directly set but calculated.
1032 // For now, we assume they might be calculated on load or save by other parts of your plugin.
1033 // If not, you'd need to calculate and save them here.
1034 // Example (conceptual):
1035 // $invoice_object = $invoice_repository->find($invoice_id);
1036 // if ($invoice_object) {
1037 // $invoice_object->setItems($sample_items); // This might trigger calculations if model is designed so
1038 // // Or call a specific method: $invoice_object->recalculateAndSaveTotals();
1039 // }
1040
1041 wp_send_json_success([
1042 'message' => __('Sample invoice created successfully!', 'easy-invoice'),
1043 'invoice_id' => $invoice_id,
1044 'edit_link' => admin_url('admin.php?page=easy-invoice-builder&id=' . $invoice_id)
1045 ]);
1046
1047 } catch (\Exception $e) {
1048 wp_send_json_error([
1049 'message' => __('Error creating sample invoice:', 'easy-invoice') . ' ' . $e->getMessage()
1050 ], 500);
1051 }
1052 }
1053
1054 /**
1055 * AJAX handler for creating a new invoice with title
1056 */
1057 public function ajax_create_new_invoice() {
1058 // Security check: verify nonce
1059 check_ajax_referer('easy_invoice_nonce', 'nonce');
1060
1061 // Security check: verify user capabilities
1062 if (!current_user_can('edit_posts')) {
1063 wp_send_json_error([
1064 'message' => __('You do not have permission to create invoices.', 'easy-invoice')
1065 ], 403);
1066 return;
1067 }
1068
1069 // Get the invoice title
1070 $title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
1071
1072 if (empty($title)) {
1073 wp_send_json_error([
1074 'message' => __('Invoice title is required.', 'easy-invoice')
1075 ], 400);
1076 return;
1077 }
1078
1079 try {
1080 $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();
1081
1082 // Prepare invoice data for repository
1083 $invoice_data = [
1084 'title' => $title,
1085 'post_status' => 'draft',
1086 'issue_date' => date('Y-m-d'),
1087 'due_date' => date('Y-m-d', strtotime('+30 days')),
1088 'status' => 'draft',
1089 'invoice_template' => 'standard'
1090 ];
1091
1092 // Create the invoice using repository (this will auto-generate invoice number)
1093 $invoice = $invoice_repository->create($invoice_data);
1094
1095 if (!$invoice) {
1096 throw new \Exception('Failed to create invoice');
1097 }
1098
1099 // Debug: Check if invoice number was set
1100 $invoice_number = $invoice->getNumber();
1101 if (empty($invoice_number)) {
1102 // Force set the invoice number if it's empty
1103 $invoice_number_service = easy_invoice_get_invoice_number_service();
1104 $generated_number = $invoice_number_service->generateUniqueNumber();
1105 $invoice->setNumber($generated_number);
1106 $invoice->save();
1107 }
1108
1109 wp_send_json_success([
1110 'message' => __('Invoice created successfully!', 'easy-invoice'),
1111 'invoice_id' => $invoice->getId(),
1112 'invoice_number' => $invoice->getNumber(),
1113 'redirect_url' => admin_url('admin.php?page=easy-invoice-builder&invoice_id=' . $invoice->getId())
1114 ]);
1115
1116 } catch (\Exception $e) {
1117 wp_send_json_error([
1118 'message' => __('Error creating invoice:', 'easy-invoice') . ' ' . $e->getMessage()
1119 ], 500);
1120 }
1121 }
1122
1123 /**
1124 * Handle search clients AJAX request
1125 *
1126 * @since 1.0.0
1127 */
1128 public function handleSearchClients(): void {
1129 // Verify nonce
1130 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_nonce')) {
1131 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1132 }
1133
1134 // Check permissions
1135 if (!current_user_can('manage_options')) {
1136 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
1137 }
1138
1139 $query = sanitize_text_field($_POST['query'] ?? '');
1140
1141 // Get client repository
1142 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1143
1144 // If query is empty, get all clients
1145 if (empty($query)) {
1146 $clients = $client_repository->all();
1147 } else {
1148 // Search clients by name, email, or company
1149 $clients = $client_repository->search($query);
1150 }
1151
1152 $results = [];
1153 foreach ($clients as $client) {
1154 $results[] = [
1155 'id' => $client->getId(),
1156 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()),
1157 'email' => $client->getEmail(),
1158 'company' => $client->getBusinessClientName(),
1159 'phone' => $client->getExtraInfo(),
1160 'website' => $client->getWebsite(),
1161 'address' => $client->getAddress()
1162 ];
1163 }
1164
1165 wp_send_json_success($results);
1166 }
1167 }