PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.4
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.4
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 / ReportController.php

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

1,041 lines 46.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Report Controller Class
4 *
5 * @package EasyInvoice
6 * @subpackage Controllers
7 * @since 1.0.0
8 */
9
10 namespace EasyInvoice\Controllers;
11
12 use EasyInvoice\Constants\PagesSlugs;
13 use EasyInvoice\Providers\InvoiceServiceProvider;
14 use EasyInvoice\Providers\ClientServiceProvider;
15
16 /**
17 * ReportController handles all reporting functionality
18 *
19 * @since 1.0.0
20 */
21 class ReportController extends BaseController {
22
23 /**
24 * Initialize the controller
25 *
26 * @since 1.0.0
27 * @return void
28 */
29 public function init() {
30 // Initialize report functionality
31 add_action( 'wp_ajax_easy_invoice_get_report_data', array( $this, 'getReportData' ) );
32
33 // Register scripts for the reports page
34 add_action( 'admin_enqueue_scripts', array( $this, 'enqueueReportScripts' ) );
35 }
36
37 /**
38 * Enqueue scripts and styles for the reports page
39 *
40 * @since 1.0.0
41 * @param string $hook The current admin page
42 * @return void
43 */
44 public function enqueueReportScripts( $hook ) {
45 if ( empty( $hook ) || strpos( $hook, PagesSlugs::REPORTS ) === false ) {
46 return;
47 }
48
49 // Register Chart.js from local copy rather than CDN
50 wp_register_script(
51 'chartjs',
52 EASY_INVOICE_PLUGIN_URL . 'assets/js/vendors/chart.min.js',
53 array(),
54 '3.9.1',
55 true
56 );
57
58 // Add fallback to CDN if local file fails
59 wp_add_inline_script('chartjs', '
60 if (typeof Chart === "undefined") {
61 var script = document.createElement("script");
62 script.src = "https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js";
63 script.onload = function() {
64 // Chart.js loaded from CDN
65 };
66 script.onerror = function() {
67 // Failed to load Chart.js from CDN
68 };
69 document.head.appendChild(script);
70 }
71 ');
72
73 // Register reports script
74 wp_register_script(
75 'easy-invoice-reports',
76 EASY_INVOICE_PLUGIN_URL . 'assets/js/reports.js',
77 array( 'jquery', 'chartjs' ),
78 EASY_INVOICE_VERSION,
79 true
80 );
81
82 // Enqueue the scripts
83 wp_enqueue_script( 'chartjs' );
84 wp_enqueue_script( 'easy-invoice-reports' );
85 }
86
87 /**
88 * Display method implementation
89 *
90 * @since 1.0.0
91 * @param array $args Display arguments
92 * @return void
93 */
94 public function display( array $args = [] ) {
95 // Check if this is a premium feature
96 if ( ! easy_invoice_has_pro() ) {
97 // Show premium popup instead of reports page
98 $this->displayPremiumPopup();
99 return;
100 }
101
102 $page = $args['page'] ?? 'easy-invoice-reports';
103
104 // Get report data
105 $report_data = $this->getReportData();
106
107 // Display the reports page
108 $this->displayReportsPage( $report_data );
109 }
110
111 /**
112 * Display premium popup for reports feature
113 */
114 private function displayPremiumPopup() {
115 ?>
116 <div class="min-h-screen bg-gray-50 flex items-center justify-center p-4">
117 <div class="max-w-lg w-full bg-white rounded-lg shadow-xl overflow-hidden">
118 <!-- Header -->
119 <div class="bg-gradient-to-r from-indigo-600 to-purple-600 px-6 py-8 text-center">
120 <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-white bg-opacity-20 mb-4">
121 <i class="fas fa-chart-bar text-white text-2xl"></i>
122 </div>
123 <h3 class="text-2xl font-bold text-white mb-2">Detailed Reports</h3>
124 <p class="text-indigo-100 text-sm">
125 Unlock powerful insights into your business performance
126 </p>
127 </div>
128
129 <!-- Content -->
130 <div class="px-6 py-8">
131 <div class="space-y-4 mb-8">
132 <div class="flex items-center text-sm text-gray-700">
133 <div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
134 <i class="fas fa-check text-green-600 text-xs"></i>
135 </div>
136 <span>Revenue analysis and trends</span>
137 </div>
138 <div class="flex items-center text-sm text-gray-700">
139 <div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
140 <i class="fas fa-check text-green-600 text-xs"></i>
141 </div>
142 <span>Payment statistics and status tracking</span>
143 </div>
144 <div class="flex items-center text-sm text-gray-700">
145 <div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
146 <i class="fas fa-check text-green-600 text-xs"></i>
147 </div>
148 <span>Monthly revenue charts and visualizations</span>
149 </div>
150 <div class="flex items-center text-sm text-gray-700">
151 <div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
152 <i class="fas fa-check text-green-600 text-xs"></i>
153 </div>
154 <span>Invoice status distribution analysis</span>
155 </div>
156 <div class="flex items-center text-sm text-gray-700">
157 <div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
158 <i class="fas fa-check text-green-600 text-xs"></i>
159 </div>
160 <span>Top clients and revenue analysis</span>
161 </div>
162 <div class="flex items-center text-sm text-gray-700">
163 <div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
164 <i class="fas fa-check text-green-600 text-xs"></i>
165 </div>
166 <span>Export reports to CSV, Excel, and PDF</span>
167 </div>
168 </div>
169
170 <!-- Action Buttons -->
171 <div class="space-y-3">
172 <a href="<?php echo admin_url('admin.php?page=easy-invoice-settings&tab=premium'); ?>" class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200">
173 <i class="fas fa-crown mr-2"></i>
174 Upgrade to Easy Invoice Pro
175 </a>
176 <a href="<?php echo admin_url('admin.php?page=easy-invoice'); ?>" class="w-full inline-flex items-center justify-center px-6 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200">
177 <i class="fas fa-arrow-left mr-2"></i>
178 Back to Dashboard
179 </a>
180 </div>
181
182 <!-- Footer -->
183 <div class="mt-6 text-center">
184 <p class="text-xs text-gray-500">
185 Get comprehensive insights into your business with advanced reporting features
186 </p>
187 </div>
188 </div>
189 </div>
190 </div>
191 <?php
192 }
193
194 /**
195 * Display reports page
196 *
197 * @since 1.0.0
198 * @return void
199 */
200 protected function displayReportsPage( $report_data = [] ) {
201 // Check user capability
202 $error = $this->checkCapability();
203 if ( is_wp_error( $error ) ) {
204 wp_die( $error );
205 }
206
207 // Get date range filters
208 $start_date = isset( $_GET['start_date'] ) ? sanitize_text_field( $_GET['start_date'] ) : date( 'Y-m-d', strtotime( '-30 days' ) );
209 $end_date = isset( $_GET['end_date'] ) ? sanitize_text_field( $_GET['end_date'] ) : date( 'Y-m-d' );
210
211 // Get repositories
212 $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();
213 $client_repository = ClientServiceProvider::getClientRepository();
214
215 // Get data for reports
216 $summary_stats = $this->getSummaryStats( $invoice_repository, $client_repository, $start_date, $end_date );
217 $monthly_revenue = $this->getMonthlySummary( $invoice_repository, $start_date, $end_date );
218 $invoice_status = $this->getInvoiceStatusSummary( $invoice_repository, $start_date, $end_date );
219 $top_clients = $this->getTopClients( $invoice_repository, $client_repository );
220
221 // Generate payment and invoice reports
222 $payment_report = $this->getPaymentReport( $start_date, $end_date );
223 $invoice_report = $this->getInvoiceReport( $start_date, $end_date );
224
225 // Prepare data for JavaScript
226 $reports_data = array(
227 'monthly_revenue' => $monthly_revenue,
228 'invoice_status' => $invoice_status,
229 'payment_report' => $payment_report,
230 'invoice_report' => $invoice_report,
231 'start_date' => $start_date,
232 'end_date' => $end_date,
233 'ajax_url' => admin_url( 'admin-ajax.php' ),
234 'nonce' => wp_create_nonce( 'easy_invoice_reports_nonce' )
235 );
236
237 // Localize the script with data
238 wp_localize_script( 'easy-invoice-reports', 'easy_invoice_reports', $reports_data );
239
240 // Display the template
241 $this->displayTemplate(
242 EASY_INVOICE_PLUGIN_DIR . 'templates/reports-page.php',
243 [
244 'start_date' => $start_date,
245 'end_date' => $end_date,
246 'summary_stats' => $summary_stats,
247 'monthly_revenue' => $monthly_revenue,
248 'invoice_status' => $invoice_status,
249 'top_clients' => $top_clients,
250 'payment_report' => $payment_report,
251 'invoice_report' => $invoice_report
252 ]
253 );
254 }
255
256 /**
257 * Get report data via AJAX
258 *
259 * @since 1.0.0
260 * @return void
261 */
262 public function getReportData() {
263 // Only handle AJAX requests
264 if ( ! wp_doing_ajax() ) {
265 return;
266 }
267
268 // Check nonce
269 if ( ! isset( $_POST['nonce'] ) || ! $this->handleAjaxSecurity( $_POST['nonce'] ) ) {
270 wp_send_json_error( array( 'message' => 'Security verification failed. Please refresh the page and try again.', 'code' => 'invalid_nonce' ) );
271 return;
272 }
273
274 $report_type = isset( $_POST['report_type'] ) ? sanitize_text_field( $_POST['report_type'] ) : '';
275 $start_date = isset( $_POST['start_date'] ) ? sanitize_text_field( $_POST['start_date'] ) : date( 'Y-m-d', strtotime( '-30 days' ) );
276 $end_date = isset( $_POST['end_date'] ) ? sanitize_text_field( $_POST['end_date'] ) : date( 'Y-m-d' );
277
278 // Get repositories
279 $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();
280 $client_repository = ClientServiceProvider::getClientRepository();
281
282 $response = array();
283
284 switch ( $report_type ) {
285 case 'monthly_revenue':
286 $response = $this->getMonthlySummary( $invoice_repository, $start_date, $end_date );
287 break;
288
289 case 'invoice_status':
290 $response = $this->getInvoiceStatusSummary( $invoice_repository, $start_date, $end_date );
291 break;
292
293 case 'top_clients':
294 $response = $this->getTopClients( $invoice_repository, $client_repository );
295 break;
296
297 case 'summary_stats':
298 $response = $this->getSummaryStats( $invoice_repository, $client_repository, $start_date, $end_date );
299 break;
300
301 case 'payment_report':
302 $response = $this->getPaymentReport( $start_date, $end_date );
303 break;
304
305 case 'invoice_report':
306 $response = $this->getInvoiceReport( $start_date, $end_date );
307 break;
308
309 default:
310 $response = array( 'error' => 'Invalid report type' );
311 break;
312 }
313
314 wp_send_json_success( $response );
315 }
316
317 /**
318 * Get summary statistics
319 *
320 * @since 1.0.0
321 * @param object $invoice_repository Invoice repository
322 * @param object $client_repository Client repository
323 * @param string $start_date Start date
324 * @param string $end_date End date
325 * @return array Summary statistics
326 */
327 private function getSummaryStats( $invoice_repository, $client_repository, $start_date = '', $end_date = '' ) {
328 try {
329 // Get all invoices
330 $all_invoices = $invoice_repository->all();
331 $paid_invoices = $invoice_repository->findByStatus( 'paid' );
332
333 // Calculate total revenue by currency
334 $revenue_by_currency = [];
335
336 // First, get all currencies that exist in the system
337 $all_currencies = [];
338
339 foreach ($all_invoices as $invoice) {
340 $currency_code = $invoice->getCurrencyCode();
341
342 // If currency is empty or "global", get the actual currency that was used
343 if (empty($currency_code) || $currency_code === 'global') {
344 // Get the actual currency from invoice meta
345 $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true);
346 $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
347 }
348
349 // If currency is still "global", use the global setting
350 if ($currency_code === 'global') {
351 $currency_code = get_option('easy_invoice_currency_code', 'USD');
352 }
353
354 // Normalize currency code to uppercase for consistent grouping
355 $currency_code = strtoupper($currency_code);
356
357 if (!empty($currency_code)) {
358 $all_currencies[$currency_code] = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
359 }
360 }
361
362 // Initialize revenue for all currencies found
363 foreach ($all_currencies as $currency_code => $currency_symbol) {
364 $revenue_by_currency[$currency_code] = [
365 'amount' => 0,
366 'symbol' => $currency_symbol
367 ];
368 }
369
370 // Sum revenue from paid invoices
371 foreach ($paid_invoices as $invoice) {
372 $currency_code = $invoice->getCurrencyCode();
373
374 // If currency is empty or "global", get the actual currency that was used
375 if (empty($currency_code) || $currency_code === 'global') {
376 // Get the actual currency from invoice meta
377 $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true);
378 $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
379 }
380
381 // If currency is still "global", use the global setting
382 if ($currency_code === 'global') {
383 $currency_code = get_option('easy_invoice_currency_code', 'USD');
384 }
385
386 // Normalize currency code to uppercase for consistent grouping
387 $currency_code = strtoupper($currency_code);
388
389 if (!empty($currency_code)) {
390 $amount = $invoice->getTotal();
391 if (!isset($revenue_by_currency[$currency_code])) {
392 $revenue_by_currency[$currency_code] = [
393 'amount' => 0,
394 'symbol' => \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code)
395 ];
396 }
397 $revenue_by_currency[$currency_code]['amount'] += $amount;
398 }
399 }
400
401 // Get other statistics
402 $total_invoices = count($all_invoices);
403 $active_clients = count($client_repository->all());
404 $avg_payment_time = 30; // Default value, could be calculated from actual payment data
405
406 return [
407 'total_revenue' => $revenue_by_currency,
408 'total_invoices' => $total_invoices,
409 'active_clients' => $active_clients,
410 'avg_payment_time' => $avg_payment_time
411 ];
412 } catch ( \Exception $e ) {
413 return [
414 'total_revenue' => [],
415 'total_invoices' => 0,
416 'active_clients' => 0,
417 'avg_payment_time' => 0
418 ];
419 }
420 }
421
422 /**
423 * Get monthly summary data
424 *
425 * @since 1.0.0
426 * @param object $invoice_repository Invoice repository
427 * @param string $start_date Optional start date
428 * @param string $end_date Optional end date
429 * @return array Monthly revenue data
430 */
431 private function getMonthlySummary( $invoice_repository, $start_date = '', $end_date = '' ) {
432 try {
433 // Calculate date range for the last 12 months
434 $current_date = new \DateTime();
435 $start_date = clone $current_date;
436 $start_date->modify('-11 months');
437 $start_date->setTime(0, 0, 0);
438
439 // Initialize monthly revenue array
440 $monthly_revenue = [];
441
442 // Initialize all months in the range
443 for ($i = 0; $i < 12; $i++) {
444 $month_date = clone $start_date;
445 $month_date->modify("+$i months");
446 $month_key = $month_date->format('M Y');
447 $monthly_revenue[$month_key] = [];
448 }
449
450 // Query payments for the date range
451 $payment_query = new \WP_Query([
452 'post_type' => 'easy_invoice_payment',
453 'post_status' => 'publish',
454 'meta_query' => [
455 [
456 'key' => '_status',
457 'value' => ['completed', 'approved', 'paid'],
458 'compare' => 'IN'
459 ],
460 [
461 'key' => '_amount',
462 'value' => '0',
463 'compare' => '>'
464 ]
465 ],
466 'date_query' => [
467 [
468 'after' => $start_date->format('Y-m-d'),
469 'inclusive' => true
470 ]
471 ],
472 'posts_per_page' => -1
473 ]);
474
475 if ($payment_query->have_posts()) {
476 while ($payment_query->have_posts()) {
477 $payment_query->the_post();
478 $payment_id = get_the_ID();
479
480 $amount = floatval(get_post_meta($payment_id, '_amount', true));
481 $currency = get_post_meta($payment_id, '_currency', true);
482 $payment_date = get_the_date('Y-m-d', $payment_id);
483
484 // Handle "global" currency
485 if (empty($currency) || $currency === 'global') {
486 $currency = get_option('easy_invoice_currency_code', 'USD');
487 }
488
489 // Normalize currency to uppercase
490 $currency = strtoupper($currency);
491
492 if ($amount > 0 && !empty($currency)) {
493 $date_obj = new \DateTime($payment_date);
494 $month_key = $date_obj->format('M Y');
495
496 if (!isset($monthly_revenue[$month_key][$currency])) {
497 $monthly_revenue[$month_key][$currency] = [
498 'amount' => 0,
499 'symbol' => \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency)
500 ];
501 }
502
503 $monthly_revenue[$month_key][$currency]['amount'] += $amount;
504 }
505 }
506 wp_reset_postdata();
507 }
508
509 return $monthly_revenue;
510 } catch ( \Exception $e ) {
511 return [];
512 }
513 }
514
515 /**
516 * Get invoice status summary
517 *
518 * @since 1.0.0
519 * @param object $invoice_repository Invoice repository
520 * @param string $start_date Optional start date
521 * @param string $end_date Optional end date
522 * @return array Invoice status data
523 */
524 private function getInvoiceStatusSummary( $invoice_repository, $start_date = '', $end_date = '' ) {
525 try {
526 // Get all invoices first
527 $invoices = $invoice_repository->all();
528
529 // Filter by date range if provided
530 if (!empty($start_date) || !empty($end_date)) {
531 $filtered_invoices = [];
532 foreach ($invoices as $invoice) {
533 $invoice_date = $invoice->getInvoiceDate();
534
535 // If invoice has a date, check if it's within the range
536 if ($invoice_date) {
537 $invoice_timestamp = strtotime($invoice_date);
538 $start_timestamp = !empty($start_date) ? strtotime($start_date) : 0;
539 $end_timestamp = !empty($end_date) ? strtotime($end_date . ' 23:59:59') : PHP_INT_MAX;
540
541 if ($invoice_timestamp >= $start_timestamp && $invoice_timestamp <= $end_timestamp) {
542 $filtered_invoices[] = $invoice;
543 }
544 } else {
545 // If invoice has no date, include it in the results (don't filter out)
546 $filtered_invoices[] = $invoice;
547 }
548 }
549 $invoices = $filtered_invoices;
550 }
551 $status_counts = [
552 'paid' => 0,
553 'unpaid' => 0,
554 'overdue' => 0,
555 'draft' => 0,
556 'canceled' => 0
557 ];
558
559 foreach ($invoices as $invoice) {
560 $status = $invoice->getStatus();
561
562 if (in_array($status, ['paid', 'completed'])) {
563 $status_counts['paid']++;
564 } elseif ($status === 'unpaid') {
565 // Check if overdue
566 $due_date = $invoice->getDueDate();
567 if ($due_date && strtotime($due_date) < current_time('timestamp')) {
568 $status_counts['overdue']++;
569 } else {
570 $status_counts['unpaid']++;
571 }
572 } elseif ($status === 'draft') {
573 $status_counts['draft']++;
574 } elseif (in_array($status, ['canceled', 'cancelled'])) {
575 $status_counts['canceled']++;
576 }
577 }
578
579
580 $total_invoices = count($invoices);
581 $percentages = [];
582
583 foreach ($status_counts as $status => $count) {
584 $percentages[$status] = $total_invoices > 0 ? round(($count / $total_invoices) * 100) : 0;
585 }
586
587 return [
588 'percentages' => $percentages,
589 'counts' => $status_counts
590 ];
591 } catch ( \Exception $e ) {
592 return [
593 'percentages' => [],
594 'counts' => []
595 ];
596 }
597 }
598
599 /**
600 * Get top clients by revenue
601 *
602 * @since 1.0.0
603 * @param object $invoice_repository Invoice repository
604 * @param object $client_repository Client repository
605 * @param string $start_date Optional start date
606 * @param string $end_date Optional end date
607 * @return array Top clients data
608 */
609 private function getTopClients( $invoice_repository, $client_repository, $start_date = '', $end_date = '' ) {
610 try {
611 $invoices = $invoice_repository->all();
612 $clients = $client_repository->all();
613
614 $client_revenue = [];
615
616 foreach ($clients as $client) {
617 $client_id = $client->getId();
618 $client_revenue[$client_id] = [
619 'id' => $client_id,
620 'name' => $client->getName(),
621 'email' => $client->getEmail(),
622 'total_amount' => [],
623 'total_invoices' => 0,
624 'last_invoice' => ''
625 ];
626 }
627
628 foreach ($invoices as $invoice) {
629 $client_id = $invoice->getClientId();
630 if (isset($client_revenue[$client_id])) {
631 $amount = $invoice->getTotal();
632 $currency = $invoice->getCurrencyCode();
633
634 // Handle "global" currency
635 if (empty($currency) || $currency === 'global') {
636 $currency = get_option('easy_invoice_currency_code', 'USD');
637 }
638
639 // Normalize currency to uppercase
640 $currency = strtoupper($currency);
641
642 if (!isset($client_revenue[$client_id]['total_amount'][$currency])) {
643 $client_revenue[$client_id]['total_amount'][$currency] = [
644 'amount' => 0,
645 'symbol' => \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency)
646 ];
647 }
648
649 $client_revenue[$client_id]['total_amount'][$currency]['amount'] += $amount;
650 $client_revenue[$client_id]['total_invoices']++;
651
652 // Track last invoice date
653 $issue_date = $invoice->getIssueDate();
654 if ($issue_date && (empty($client_revenue[$client_id]['last_invoice']) || $issue_date > $client_revenue[$client_id]['last_invoice'])) {
655 $client_revenue[$client_id]['last_invoice'] = $issue_date;
656 }
657 }
658 }
659
660 // Sort by total revenue (USD first, then other currencies)
661 uasort($client_revenue, function($a, $b) {
662 $a_total = isset($a['total_amount']['USD']) ? $a['total_amount']['USD']['amount'] : 0;
663 $b_total = isset($b['total_amount']['USD']) ? $b['total_amount']['USD']['amount'] : 0;
664 return $b_total <=> $a_total;
665 });
666
667 // Return top 10 clients
668 return array_slice($client_revenue, 0, 10, true);
669 } catch ( \Exception $e ) {
670 return [];
671 }
672 }
673
674 /**
675 * Get payment report data
676 *
677 * @since 1.0.0
678 * @param string $start_date Start date for report
679 * @param string $end_date End date for report
680 * @return array Payment report data
681 */
682 private function getPaymentReport( $start_date = '', $end_date = '' ) {
683 global $wpdb;
684
685 $payment_table = $wpdb->prefix . 'posts';
686 $payment_meta_table = $wpdb->prefix . 'postmeta';
687
688 $start = $start_date ? strtotime( $start_date ) : strtotime( '-1 year' );
689 $end = $end_date ? strtotime( $end_date ) : current_time( 'timestamp' );
690
691 // Format dates for SQL query
692 $start_date_formatted = date( 'Y-m-d 00:00:00', $start );
693 $end_date_formatted = date( 'Y-m-d 23:59:59', $end );
694
695 // Query to get payments within date range
696 $query = $wpdb->prepare(
697 "SELECT p.ID, p.post_date,
698 MAX(CASE WHEN pm.meta_key = '_invoice_id' THEN pm.meta_value ELSE NULL END) as invoice_id,
699 MAX(CASE WHEN pm.meta_key = '_amount' THEN pm.meta_value ELSE NULL END) as amount,
700 MAX(CASE WHEN pm.meta_key = '_payment_method' THEN pm.meta_value ELSE NULL END) as payment_method,
701 MAX(CASE WHEN pm.meta_key = '_status' THEN pm.meta_value ELSE NULL END) as status,
702 MAX(CASE WHEN pm.meta_key = '_transaction_id' THEN pm.meta_value ELSE NULL END) as transaction_id,
703 MAX(CASE WHEN pm.meta_key = '_currency' THEN pm.meta_value ELSE NULL END) as currency,
704 MAX(CASE WHEN pm.meta_key = '_currency_symbol' THEN pm.meta_value ELSE NULL END) as currency_symbol
705 FROM $payment_table p
706 LEFT JOIN $payment_meta_table pm ON p.ID = pm.post_id
707 WHERE p.post_type = %s
708 AND p.post_date BETWEEN %s AND %s
709 GROUP BY p.ID
710 ORDER BY p.post_date DESC
711 LIMIT 100",
712 'easy_invoice_payment',
713 $start_date_formatted,
714 $end_date_formatted
715 );
716
717 $payments = $wpdb->get_results( $query, ARRAY_A );
718
719 $report_data = [
720 'payments' => [],
721 'total_amount' => 0,
722 'count' => 0,
723 'by_method' => [],
724 'by_status' => []
725 ];
726
727 if ( $payments ) {
728 foreach ( $payments as $payment ) {
729 $payment_method = $payment['payment_method'] ?? 'unknown';
730 $status = $payment['status'] ?? 'unknown';
731 $amount = floatval( $payment['amount'] ?? 0 );
732 $currency = $payment['currency'] ?? 'USD';
733 $currency_symbol = $payment['currency_symbol'] ?? '$';
734
735 // Normalize currency code to uppercase for consistent grouping
736 $currency = strtoupper($currency);
737
738 // Handle "global" currency by getting the actual global setting
739 if (empty($currency) || strtolower($currency) === 'global') {
740 $currency = get_option('easy_invoice_currency_code', 'USD');
741 }
742
743 // Additional check: if currency is still "global" after replacement, use the global setting
744 if (strtolower($currency) === 'global') {
745 $currency = get_option('easy_invoice_currency_code', 'USD');
746 }
747
748 // Get proper currency symbol
749 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency);
750
751 // Add to total (we'll handle multi-currency totals separately)
752 $report_data['count']++;
753
754 // Track by payment method with currency breakdown
755 if ( ! isset( $report_data['by_method'][$payment_method] ) ) {
756 $report_data['by_method'][$payment_method] = [
757 'count' => 0,
758 'amounts_by_currency' => []
759 ];
760 }
761
762 // Initialize currency for this payment method if not exists
763 if ( ! isset( $report_data['by_method'][$payment_method]['amounts_by_currency'][$currency] ) ) {
764 $report_data['by_method'][$payment_method]['amounts_by_currency'][$currency] = [
765 'amount' => 0,
766 'symbol' => $currency_symbol
767 ];
768 }
769
770 $report_data['by_method'][$payment_method]['amounts_by_currency'][$currency]['amount'] += $amount;
771 $report_data['by_method'][$payment_method]['count']++;
772
773 // Track by status with currency breakdown
774 if ( ! isset( $report_data['by_status'][$status] ) ) {
775 $report_data['by_status'][$status] = [
776 'count' => 0,
777 'amounts_by_currency' => []
778 ];
779 }
780
781 // Initialize currency for this status if not exists
782 if ( ! isset( $report_data['by_status'][$status]['amounts_by_currency'][$currency] ) ) {
783 $report_data['by_status'][$status]['amounts_by_currency'][$currency] = [
784 'amount' => 0,
785 'symbol' => $currency_symbol
786 ];
787 }
788
789 $report_data['by_status'][$status]['amounts_by_currency'][$currency]['amount'] += $amount;
790 $report_data['by_status'][$status]['count']++;
791
792 // Get invoice number if available
793 $invoice_number = '';
794 if ( ! empty( $payment['invoice_id'] ) ) {
795 $invoice_number = get_post_meta( $payment['invoice_id'], '_invoice_number', true );
796
797 // Provide a fallback format if no invoice number is found
798 if ( empty( $invoice_number ) ) {
799 $invoice_number = 'INV-' . $payment['invoice_id'];
800 }
801 }
802
803 // Format payment for report
804 $report_data['payments'][] = [
805 'id' => $payment['ID'],
806 'date' => $payment['post_date'],
807 'invoice_id' => $payment['invoice_id'],
808 'invoice_number' => $invoice_number ?: '#' . $payment['invoice_id'],
809 'amount' => $amount,
810 'payment_method' => $payment_method,
811 'status' => $status,
812 'transaction_id' => $payment['transaction_id'] ?? '',
813 'currency' => $currency,
814 'currency_symbol' => $currency_symbol
815 ];
816 }
817 }
818
819 return $report_data;
820 }
821
822 /**
823 * Get invoice report data
824 *
825 * @since 1.0.0
826 * @param string $start_date Start date for report
827 * @param string $end_date End date for report
828 * @return array Invoice report data
829 */
830 private function getInvoiceReport( $start_date = '', $end_date = '' ) {
831 global $wpdb;
832
833 $invoice_table = $wpdb->prefix . 'posts';
834 $invoice_meta_table = $wpdb->prefix . 'postmeta';
835
836 $start = $start_date ? strtotime( $start_date ) : strtotime( '-1 year' );
837 $end = $end_date ? strtotime( $end_date ) : current_time( 'timestamp' );
838
839 // Format dates for SQL query
840 $start_date_formatted = date( 'Y-m-d 00:00:00', $start );
841 $end_date_formatted = date( 'Y-m-d 23:59:59', $end );
842
843 try {
844 // Query to get invoices within date range - using the correct post type "easy_invoice"
845 // First, let's get all invoices without date filtering to see if there are any
846 $query = $wpdb->prepare(
847 "SELECT p.ID, p.post_date, p.post_status, p.post_title,
848 MAX(CASE WHEN pm.meta_key = '_easy_invoice_number' THEN pm.meta_value ELSE NULL END) as invoice_number,
849 MAX(CASE WHEN pm.meta_key = '_easy_invoice_total' THEN pm.meta_value ELSE NULL END) as total,
850 MAX(CASE WHEN pm.meta_key = '_invoice_total' THEN pm.meta_value ELSE NULL END) as invoice_total,
851 MAX(CASE WHEN pm.meta_key = '_payment_status' THEN pm.meta_value ELSE NULL END) as payment_status,
852 MAX(CASE WHEN pm.meta_key = '_easy_invoice_client_id' THEN pm.meta_value ELSE NULL END) as client_id,
853 MAX(CASE WHEN pm.meta_key = '_easy_invoice_issue_date' THEN pm.meta_value ELSE NULL END) as issue_date,
854 MAX(CASE WHEN pm.meta_key = '_easy_invoice_due_date' THEN pm.meta_value ELSE NULL END) as due_date,
855 MAX(CASE WHEN pm.meta_key = '_easy_invoice_currency_code' THEN pm.meta_value ELSE NULL END) as currency_code,
856 MAX(CASE WHEN pm.meta_key = '_currency_code' THEN pm.meta_value ELSE NULL END) as currency_code_alt,
857 MAX(CASE WHEN pm.meta_key = '_easy_invoice_customer_name' THEN pm.meta_value ELSE NULL END) as customer_name,
858 MAX(CASE WHEN pm.meta_key = 'customer_name' THEN pm.meta_value ELSE NULL END) as customer_name_alt,
859 MAX(CASE WHEN pm.meta_key = '_easy_invoice_client_name' THEN pm.meta_value ELSE NULL END) as client_name,
860 MAX(CASE WHEN pm.meta_key = 'client_name' THEN pm.meta_value ELSE NULL END) as client_name_alt
861 FROM $invoice_table p
862 LEFT JOIN $invoice_meta_table pm ON p.ID = pm.post_id
863 WHERE p.post_type = %s
864 GROUP BY p.ID
865 ORDER BY p.post_date DESC
866 LIMIT 100",
867 'easy_invoice'
868 );
869
870 $invoices = $wpdb->get_results( $query, ARRAY_A );
871 } catch ( \Exception $e ) {
872 $invoices = array();
873 }
874
875 $report_data = [
876 'invoices' => [],
877 'count' => 0,
878 'total_amounts_by_currency' => [], // Track totals by currency
879 'by_status' => [
880 'paid' => ['count' => 0, 'amounts_by_currency' => []],
881 'unpaid' => ['count' => 0, 'amounts_by_currency' => []],
882 'overdue' => ['count' => 0, 'amounts_by_currency' => []],
883 'draft' => ['count' => 0, 'amounts_by_currency' => []],
884 'canceled' => ['count' => 0, 'amounts_by_currency' => []],
885 'other' => ['count' => 0, 'amounts_by_currency' => []]
886 ]
887 ];
888
889 if ( $invoices ) {
890 foreach ( $invoices as $invoice ) {
891 $payment_status = $invoice['payment_status'] ?? 'unpaid';
892
893 // Check multiple possible total fields
894 $total = 0;
895 if ( ! empty( $invoice['total'] ) ) {
896 $total = floatval( $invoice['total'] );
897 } elseif ( ! empty( $invoice['invoice_total'] ) ) {
898 $total = floatval( $invoice['invoice_total'] );
899 }
900
901 // Get currency information
902 $currency_code = $invoice['currency_code'] ?? $invoice['currency_code_alt'] ?? 'USD';
903 $currency_code = strtoupper($currency_code);
904
905 // Handle "global" currency by getting the actual global setting
906 if (empty($currency_code) || strtolower($currency_code) === 'global') {
907 $currency_code = get_option('easy_invoice_currency_code', 'USD');
908 }
909
910 // Additional check: if currency is still "global" after replacement, use the global setting
911 if (strtolower($currency_code) === 'global') {
912 $currency_code = get_option('easy_invoice_currency_code', 'USD');
913 }
914
915 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
916
917 // Add to count
918 $report_data['count']++;
919
920 // Map payment statuses to standard categories for report
921 $status_key = 'other';
922 if ( in_array( $payment_status, ['paid', 'completed'] ) ) {
923 $status_key = 'paid';
924 } elseif ( $payment_status === 'unpaid' ) {
925 $status_key = 'unpaid';
926
927 // Check if overdue
928 if ( ! empty( $invoice['due_date'] ) ) {
929 $due_date = strtotime( $invoice['due_date'] );
930 if ( $due_date && $due_date < current_time( 'timestamp' ) ) {
931 $status_key = 'overdue';
932 $payment_status = 'overdue';
933 }
934 }
935 } elseif ( $invoice['post_status'] === 'draft' ) {
936 $status_key = 'draft';
937 $payment_status = 'draft';
938 } elseif ( $payment_status === 'canceled' || $invoice['post_status'] === 'trash' ) {
939 $status_key = 'canceled';
940 $payment_status = 'canceled';
941 }
942
943 // Track by status with currency breakdown
944 if ( ! isset( $report_data['by_status'][$status_key]['amounts_by_currency'][$currency_code] ) ) {
945 $report_data['by_status'][$status_key]['amounts_by_currency'][$currency_code] = [
946 'amount' => 0,
947 'symbol' => $currency_symbol
948 ];
949 }
950
951 $report_data['by_status'][$status_key]['amounts_by_currency'][$currency_code]['amount'] += $total;
952 $report_data['by_status'][$status_key]['count']++;
953
954 // Track overall totals by currency
955 if (!isset($report_data['total_amounts_by_currency'][$currency_code])) {
956 $report_data['total_amounts_by_currency'][$currency_code] = [
957 'amount' => 0,
958 'symbol' => $currency_symbol
959 ];
960 }
961 $report_data['total_amounts_by_currency'][$currency_code]['amount'] += $total;
962
963 // Get client name if available - try multiple sources
964 $client_name = '';
965
966 // First try the customer_name fields from the query
967 if ( ! empty( $invoice['customer_name'] ) ) {
968 $client_name = $invoice['customer_name'];
969 } elseif ( ! empty( $invoice['customer_name_alt'] ) ) {
970 $client_name = $invoice['customer_name_alt'];
971 } elseif ( ! empty( $invoice['client_name'] ) ) {
972 $client_name = $invoice['client_name'];
973 } elseif ( ! empty( $invoice['client_name_alt'] ) ) {
974 $client_name = $invoice['client_name_alt'];
975 } elseif ( ! empty( $invoice['client_id'] ) ) {
976 // Try to get client from post
977 $client = get_post( $invoice['client_id'] );
978 if ( $client && $client->post_type === 'easy_invoice_client' ) {
979 $client_name = $client->post_title;
980 } else {
981 // Try to get client name from meta if direct post lookup fails
982 $client_name = get_post_meta( $invoice['client_id'], '_client_name', true );
983 if ( empty( $client_name ) ) {
984 // Try alternative meta key
985 $client_name = get_post_meta( $invoice['client_id'], 'client_name', true );
986 }
987 if ( empty( $client_name ) ) {
988 // Try to get client name from invoice meta
989 $client_name = get_post_meta( $invoice['ID'], '_client_name', true );
990 }
991 if ( empty( $client_name ) ) {
992 // Try customer_name from invoice meta
993 $client_name = get_post_meta( $invoice['ID'], '_easy_invoice_customer_name', true );
994 }
995 if ( empty( $client_name ) ) {
996 // Try alternative customer name meta keys
997 $client_name = get_post_meta( $invoice['ID'], 'customer_name', true );
998 }
999 if ( empty( $client_name ) ) {
1000 // Try client name from invoice meta
1001 $client_name = get_post_meta( $invoice['ID'], '_easy_invoice_client_name', true );
1002 }
1003 if ( empty( $client_name ) ) {
1004 // Try client name from invoice meta (alternative)
1005 $client_name = get_post_meta( $invoice['ID'], 'client_name', true );
1006 }
1007 }
1008 }
1009
1010 // If still no client name, try to get it from the invoice post title or other sources
1011 if ( empty( $client_name ) ) {
1012 // Try to get from invoice post title if it contains client info
1013 $invoice_post = get_post( $invoice['ID'] );
1014 if ( $invoice_post && ! empty( $invoice_post->post_title ) ) {
1015 $client_name = $invoice_post->post_title;
1016 }
1017 }
1018
1019 // Format invoice number with fallback
1020 $invoice_number = ! empty( $invoice['invoice_number'] ) ? $invoice['invoice_number'] : 'INV-' . $invoice['ID'];
1021
1022 // Format invoice for report
1023 $report_data['invoices'][] = [
1024 'id' => $invoice['ID'],
1025 'date' => $invoice['post_date'],
1026 'issue_date' => $invoice['issue_date'] ?? $invoice['post_date'],
1027 'due_date' => $invoice['due_date'] ?? '',
1028 'invoice_number' => $invoice_number,
1029 'total' => $total,
1030 'currency' => $currency_code,
1031 'currency_symbol' => $currency_symbol,
1032 'status' => $payment_status,
1033 'client_id' => $invoice['client_id'] ?? '',
1034 'client_name' => $client_name
1035 ];
1036 }
1037 }
1038
1039 return $report_data;
1040 }
1041 }