PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
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 / templates / reports-page.php

reports-page.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at templates/reports-page.php

1,514 lines 90.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // Exit if accessed directly
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6 ?>
7
8 <script type="text/javascript">
9 // Run immediately to ensure payment tab is visible before anything else
10 (function() {
11 document.addEventListener('DOMContentLoaded', function() {
12 // Force payment tab to be visible as soon as DOM is ready
13 var paymentTab = document.getElementById('payment-report-content');
14 if (paymentTab) {
15 paymentTab.style.display = 'block';
16 paymentTab.classList.remove('hidden');
17 paymentTab.classList.add('block');
18 }
19
20 // Make sure payment button is active
21 var paymentButton = document.getElementById('tab-payment-report');
22 if (paymentButton) {
23 paymentButton.classList.add('active-tab', 'border-indigo-500', 'text-indigo-600');
24 paymentButton.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
25 }
26 });
27 })();
28 </script>
29
30 <div class="p-8">
31 <div class="ei-app-page-header bg-white border-b border-gray-200 px-6" style="margin-left: -2rem; margin-top: -2rem; padding-left:2rem; padding-right:2rem; margin-right: -2rem;">
32 <div class="flex items-center justify-between">
33 <div>
34 <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Reports', 'easy-invoice'); ?></h1>
35 <p class="mt-1 text-sm text-gray-500"><?php esc_html_e('Comprehensive insights and analytics', 'easy-invoice'); ?></p>
36 </div>
37 <div class="flex items-center space-x-3">
38 <form method="get" action="<?php echo esc_url(admin_url('admin.php')); ?>" class="flex space-x-2">
39 <input type="hidden" name="page" value="easy-invoice-reports">
40 <div class="flex items-center">
41 <label for="start_date" class="mr-2 text-sm font-medium text-gray-700"><?php esc_html_e('From:', 'easy-invoice'); ?></label>
42 <input type="date" id="start_date" name="start_date" value="<?php echo esc_attr($start_date); ?>" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm transition-colors duration-200">
43 </div>
44 <div class="flex items-center">
45 <label for="end_date" class="mr-2 text-sm font-medium text-gray-700">To:</label>
46 <input type="date" id="end_date" name="end_date" value="<?php echo esc_attr($end_date); ?>" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm transition-colors duration-200">
47 </div>
48 <button type="submit" class="inline-flex items-center px-3 py-2 border border-transparent text-sm 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">
49 <i class="fas fa-filter mr-1"></i> Filter
50 </button>
51 </form>
52 <button type="button" id="export-report" class="inline-flex items-center px-4 py-2 border border-transparent text-sm 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">
53 <i class="fas fa-download mr-2"></i>
54 Export Report
55 </button>
56 </div>
57 </div>
58 </div>
59
60 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 mt-8">
61 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
62 <div class="flex items-center">
63 <div class="rounded-full bg-indigo-100 p-3 mr-4">
64 <svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
65 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
66 </svg>
67 </div>
68 <div>
69 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Revenue (paid invoices in period, net of credits)', 'easy-invoice'); ?></h3>
70 <?php if (is_array($summary_stats['total_revenue']) && !empty($summary_stats['total_revenue'])): ?>
71 <div class="mt-1">
72 <?php
73 $currency_count = count($summary_stats['total_revenue']);
74 foreach ($summary_stats['total_revenue'] as $index => $data):
75 $is_last = $index === $currency_count - 1;
76 // Get currency code from the data array or use a fallback
77 $currency_code = isset($data['currency_code']) ? $data['currency_code'] : 'USD';
78 ?>
79 <div class="<?php echo $currency_count > 1 ? 'text-sm' : 'text-lg'; ?> font-bold text-gray-900 <?php echo !$is_last ? 'mb-1' : ''; ?>">
80 <?php echo esc_html(easy_invoice_format_money($data['amount'], null, (string) ($currency_code ?? 'USD'))); ?>
81 <span class="text-xs font-normal text-gray-500"><?php echo esc_html(strtoupper($currency_code ?? 'USD')); ?></span>
82 </div>
83 <?php endforeach; ?>
84 </div>
85 <?php else: ?>
86 <p class="mt-1 text-lg font-bold text-gray-900">$0.00</p>
87 <?php endif; ?>
88 </div>
89 </div>
90 </div>
91
92 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
93 <div class="flex items-center">
94 <div class="rounded-full bg-green-100 p-3 mr-4">
95 <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
96 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
97 </svg>
98 </div>
99 <div>
100 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Total Invoices', 'easy-invoice'); ?></h3>
101 <p class="mt-1 text-2xl font-bold text-gray-900"><?php echo esc_html($summary_stats['total_invoices']); ?></p>
102 </div>
103 </div>
104 </div>
105
106 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
107 <div class="flex items-center">
108 <div class="rounded-full bg-blue-100 p-3 mr-4">
109 <svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
110 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"/>
111 <circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
112 <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"/>
113 <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"/>
114 </svg>
115 </div>
116 <div>
117 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Invoiced Clients', 'easy-invoice'); ?></h3>
118 <p class="mt-1 text-2xl font-bold text-gray-900"><?php echo esc_html($summary_stats['active_clients']); ?></p>
119 </div>
120 </div>
121 </div>
122
123 <div class="bg-white rounded-lg shadow p-6 transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
124 <div class="flex items-center">
125 <div class="rounded-full bg-yellow-100 p-3 mr-4">
126 <svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
127 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
128 </svg>
129 </div>
130 <div>
131 <h3 class="text-sm font-medium text-gray-500"><?php esc_html_e('Average Payment Time', 'easy-invoice'); ?></h3>
132 <p class="mt-1 text-2xl font-bold text-gray-900"><?php echo null === $summary_stats['avg_payment_time'] ? '&mdash;' : esc_html($summary_stats['avg_payment_time']) . ' ' . esc_html__('days', 'easy-invoice'); ?></p>
133 </div>
134 </div>
135 </div>
136 </div>
137
138 <div class="grid grid-cols-1 gap-5 lg:grid-cols-2 mb-8">
139 <div class="bg-white shadow rounded-lg p-6">
140 <h2 class="text-lg font-medium text-gray-900 mb-4"><?php esc_html_e('Monthly Revenue (payments received, last 12 months)', 'easy-invoice'); ?></h2>
141 <div class="h-64">
142 <canvas id="revenue-chart"></canvas>
143 </div>
144 </div>
145
146 <div class="bg-white shadow rounded-lg p-6">
147 <h2 class="text-lg font-medium text-gray-900 mb-4"><?php esc_html_e('Invoice Status', 'easy-invoice'); ?></h2>
148 <div class="h-64 flex items-center justify-center">
149 <div class="w-48 h-48 relative">
150 <canvas id="status-chart"></canvas>
151 </div>
152 <div class="ml-8">
153 <?php
154 $statusColors = [
155 'Paid' => 'bg-green-500',
156 'Partial' => 'bg-blue-500',
157 'Unpaid' => 'bg-yellow-500',
158 'Overdue' => 'bg-red-500',
159 'Draft' => 'bg-gray-300',
160 'Canceled' => 'bg-gray-500'
161 ];
162 $ei_status_names = [
163 'paid' => __('Paid', 'easy-invoice'), 'partial' => __('Partially paid', 'easy-invoice'), 'unpaid' => __('Unpaid', 'easy-invoice'),
164 'overdue' => __('Overdue', 'easy-invoice'), 'draft' => __('Draft', 'easy-invoice'), 'canceled' => __('Cancelled', 'easy-invoice'),
165 ];
166
167 if (isset($invoice_status['percentages']) && isset($invoice_status['counts'])) {
168 foreach ($invoice_status['percentages'] as $status => $percentage) {
169 $color = isset($statusColors[ucfirst($status)]) ? $statusColors[ucfirst($status)] : 'bg-gray-500';
170 $count = isset($invoice_status['counts'][$status]) ? $invoice_status['counts'][$status] : 0;
171 echo '<div class="flex items-center mb-2">';
172 echo '<div class="w-4 h-4 rounded-full ' . esc_attr($color) . ' mr-2"></div>';
173 echo '<span class="text-sm text-gray-600">' . esc_html($ei_status_names[$status] ?? ucfirst($status)) . ' (' . esc_html($percentage) . '% - ' . esc_html($count) . ')</span>';
174 echo '</div>';
175 }
176 }
177 ?>
178 </div>
179 </div>
180 </div>
181 </div>
182
183 <div class="bg-white shadow rounded-lg mb-8">
184 <div class="px-6 py-4 border-b border-gray-200">
185 <h2 class="text-lg font-medium text-gray-900"><?php esc_html_e('Top Clients by Revenue', 'easy-invoice'); ?></h2>
186 </div>
187 <div class="overflow-x-auto">
188 <table class="min-w-full divide-y divide-gray-200">
189 <thead class="bg-gray-50">
190 <tr>
191 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
192 Client
193 </th>
194 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
195 Total Amount
196 </th>
197 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
198 Invoices
199 </th>
200 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
201 Last Invoice
202 </th>
203 <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
204 Actions
205 </th>
206 </tr>
207 </thead>
208 <tbody class="bg-white divide-y divide-gray-200">
209 <?php if (!empty($top_clients)) : ?>
210 <?php foreach ($top_clients as $client): ?>
211 <tr class="hover:bg-gray-50">
212 <td class="px-6 py-4 whitespace-nowrap">
213 <div class="flex items-center">
214 <div class="flex-shrink-0 h-10 w-10">
215 <?php
216 $gravatar_url = '';
217 if (!empty($client['email'])) {
218 $gravatar_url = get_avatar_url($client['email'], ['size' => 40, 'default' => 'mp']);
219 }
220 if ($gravatar_url) {
221 echo '<img class="h-10 w-10 rounded-full object-cover" src="' . esc_url($gravatar_url) . '" alt="' . esc_attr($client['name']) . '" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
222 }
223 ?>
224 <div class="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center" <?php echo $gravatar_url ? 'style="display: none;"' : ''; ?>>
225 <span class="text-indigo-600 font-medium"><?php echo esc_html(substr($client['name'] ?? '', 0, 1)); ?></span>
226 </div>
227 </div>
228 <div class="ml-4">
229 <div class="text-sm font-medium text-gray-900"><?php echo esc_html($client['name']); ?></div>
230 <div class="text-sm text-gray-500"><?php echo esc_html($client['email']); ?></div>
231 </div>
232 </div>
233 </td>
234 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
235 <?php if (isset($client['total_amount']) && is_array($client['total_amount']) && !empty($client['total_amount'])): ?>
236 <?php foreach ($client['total_amount'] as $currency => $currency_data): ?>
237 <div>
238 <?php echo esc_html(easy_invoice_format_money($currency_data['amount'], null, (string) $currency)); ?>
239 <span class="text-xs text-gray-500"><?php echo esc_html($currency); ?></span>
240 </div>
241 <?php endforeach; ?>
242 <?php else: ?>
243 $0.00
244 <?php endif; ?>
245 </td>
246 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
247 <?php echo esc_html($client['total_invoices']); ?> invoices
248 </td>
249 <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
250 <?php echo esc_html(!empty($client['last_invoice']) ? gmdate('M d, Y', strtotime($client['last_invoice'])) : 'N/A'); ?>
251 </td>
252
253
254 <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
255 <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-client-view&client_id=' . $client['id'])); ?>" class="text-indigo-600 hover:text-indigo-900">View Client</a>
256 </td>
257 </tr>
258 <?php endforeach; ?>
259 <?php else: ?>
260 <tr>
261 <td colspan="5" class="px-6 py-4 text-center text-gray-500"><?php esc_html_e('No client data available', 'easy-invoice'); ?></td>
262 </tr>
263 <?php endif; ?>
264 </tbody>
265 </table>
266 </div>
267 </div>
268
269 <?php
270 /**
271 * Fires between the summary charts and the detailed reports.
272 *
273 * Chart.js is on the page as the `chartjs` handle. The Pro Reports
274 * addon adds the profit & loss statement here.
275 *
276 * @param string $start_date Y-m-d.
277 * @param string $end_date Y-m-d.
278 */
279 do_action('easy_invoice_reports_after_summary', $start_date, $end_date);
280 ?>
281
282 <div class="bg-white shadow rounded-lg">
283 <div class="border-b border-gray-200">
284 <div class="flex items-center justify-between px-6 py-3">
285 <h2 class="text-lg font-medium text-gray-900"><?php esc_html_e('Detailed Reports', 'easy-invoice'); ?></h2>
286 <div>
287 <button id="export-detailed-report" class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
288 <i class="fas fa-download mr-2"></i> Export
289 </button>
290 </div>
291 </div>
292 <nav class="flex -mb-px">
293 <button id="tab-payment-report" class="tab-button active-tab w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm border-indigo-500 text-indigo-600">
294 Payment Report
295 </button>
296 <button id="tab-invoice-report" class="tab-button w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
297 Invoice Report
298 </button>
299 </nav>
300 </div>
301
302 <div id="payment-report-content" class="tab-content block" style="display: block !important;">
303 <div class="p-4 bg-gray-50 border-b">
304 <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
305 <div class="bg-white p-4 rounded-md shadow-sm">
306 <div class="text-sm text-gray-500"><?php esc_html_e('Total Payments', 'easy-invoice'); ?></div>
307 <div class="text-xl font-bold"><?php echo esc_html($payment_report['count']); ?></div>
308 </div>
309 <div class="bg-white p-4 rounded-md shadow-sm">
310 <div class="text-sm text-gray-500"><?php esc_html_e('Total Amount', 'easy-invoice'); ?></div>
311 <div class="text-lg font-bold">
312 <?php
313 // Calculate total amount from all payment methods and currencies
314 $total_amounts_by_currency = [];
315 if (!empty($payment_report['by_method'])) {
316 foreach ($payment_report['by_method'] as $method => $data) {
317 if (isset($data['amounts_by_currency'])) {
318 foreach ($data['amounts_by_currency'] as $currency => $currency_data) {
319 if (!isset($total_amounts_by_currency[$currency])) {
320 $total_amounts_by_currency[$currency] = [
321 'amount' => 0,
322 'symbol' => $currency_data['symbol']
323 ];
324 }
325 $total_amounts_by_currency[$currency]['amount'] += $currency_data['amount'];
326 }
327 }
328 }
329 }
330
331 if (!empty($total_amounts_by_currency)): ?>
332 <?php foreach ($total_amounts_by_currency as $currency => $data): ?>
333 <div class="text-sm">
334 <?php echo esc_html(easy_invoice_format_money($data['amount'], null, (string) $currency)); ?>
335 <span class="text-xs text-gray-500"><?php echo esc_html($currency); ?></span>
336 </div>
337 <?php endforeach; ?>
338 <?php else: ?>
339 $0.00
340 <?php endif; ?>
341 </div>
342 </div>
343 <div class="bg-white p-4 rounded-md shadow-sm">
344 <div class="text-sm text-gray-500"><?php esc_html_e('Average Payment', 'easy-invoice'); ?></div>
345 <div class="text-lg font-bold">
346 <?php
347 // Calculate average payments by currency
348 $average_amounts_by_currency = [];
349 $payment_count = $payment_report['count'];
350
351 if (!empty($payment_report['by_method']) && $payment_count > 0) {
352 foreach ($payment_report['by_method'] as $method => $data) {
353 if (isset($data['amounts_by_currency'])) {
354 foreach ($data['amounts_by_currency'] as $currency => $currency_data) {
355 if (!isset($average_amounts_by_currency[$currency])) {
356 $average_amounts_by_currency[$currency] = [
357 'amount' => 0,
358 'symbol' => $currency_data['symbol']
359 ];
360 }
361 $average_amounts_by_currency[$currency]['amount'] += $currency_data['amount'];
362 }
363 }
364 }
365
366 // Calculate averages
367 foreach ($average_amounts_by_currency as $currency => $data) {
368 $average_amounts_by_currency[$currency]['amount'] = $data['amount'] / $payment_count;
369 }
370 }
371
372 if (!empty($average_amounts_by_currency)): ?>
373 <?php foreach ($average_amounts_by_currency as $currency => $data): ?>
374 <div class="text-sm">
375 <?php echo esc_html(easy_invoice_format_money($data['amount'], null, (string) $currency)); ?>
376 <span class="text-xs text-gray-500">
377 <?php
378 $display_currency = $currency;
379 if ($display_currency === 'GLOBAL') {
380 $display_currency = get_option('easy_invoice_currency_code', 'USD');
381 }
382 echo esc_html(strtoupper($display_currency));
383 ?>
384 </span>
385 </div>
386 <?php endforeach; ?>
387 <?php elseif ($payment_count > 0): ?>
388 <span class="text-sm text-gray-500"><?php esc_html_e('No currency data', 'easy-invoice'); ?></span>
389 <?php else: ?>
390 $0.00
391 <?php endif; ?>
392 </div>
393 </div>
394 </div>
395 </div>
396
397 <div class="p-4">
398 <h3 class="text-md font-medium text-gray-700 mb-3"><?php esc_html_e('Payment Methods Distribution', 'easy-invoice'); ?></h3>
399 <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
400 <div class="bg-white p-4 rounded-md shadow-sm">
401 <canvas id="payment-methods-chart" height="200"></canvas>
402 </div>
403 <div class="bg-white p-4 rounded-md shadow-sm">
404 <table class="min-w-full divide-y divide-gray-200">
405 <thead>
406 <tr>
407 <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Method', 'easy-invoice'); ?></th>
408 <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Count', 'easy-invoice'); ?></th>
409 <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Amount', 'easy-invoice'); ?></th>
410 </tr>
411 </thead>
412 <tbody>
413 <?php
414 if (!empty($payment_report['by_method'])) :
415 foreach ($payment_report['by_method'] as $method => $data) :
416 ?>
417 <tr>
418 <td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
419 <?php echo esc_html(ucfirst($method)); ?>
420 </td>
421 <td class="px-3 py-2 whitespace-nowrap text-sm text-right text-gray-500">
422 <?php echo esc_html($data['count']); ?>
423 </td>
424 <td class="px-3 py-2 whitespace-nowrap text-sm text-right text-gray-900">
425 <?php if (isset($data['amounts_by_currency']) && !empty($data['amounts_by_currency'])): ?>
426 <?php foreach ($data['amounts_by_currency'] as $currency => $currency_data): ?>
427 <div class="text-xs">
428 <?php echo esc_html(easy_invoice_format_money($currency_data['amount'], null, (string) $currency)); ?>
429 <span class="text-xs text-gray-500"><?php echo esc_html($currency); ?></span>
430 </div>
431 <?php endforeach; ?>
432 <?php else: ?>
433 $0.00
434 <?php endif; ?>
435 </td>
436 </tr>
437 <?php
438 endforeach;
439 else:
440 ?>
441 <tr>
442 <td colspan="3" class="px-3 py-4 text-center text-sm text-gray-500"><?php esc_html_e('No payment method data available', 'easy-invoice'); ?></td>
443 </tr>
444 <?php endif; ?>
445 </tbody>
446 </table>
447 </div>
448 </div>
449 </div>
450
451 <div class="p-4">
452 <h3 class="text-md font-medium text-gray-700 mb-3"><?php esc_html_e('Recent Payments', 'easy-invoice'); ?></h3>
453 <div class="overflow-x-auto">
454 <table class="min-w-full divide-y divide-gray-200">
455 <thead class="bg-gray-50">
456 <tr>
457 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Date', 'easy-invoice'); ?></th>
458 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Invoice', 'easy-invoice'); ?></th>
459 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Amount', 'easy-invoice'); ?></th>
460 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Method', 'easy-invoice'); ?></th>
461 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Status', 'easy-invoice'); ?></th>
462 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Transaction ID', 'easy-invoice'); ?></th>
463 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Actions', 'easy-invoice'); ?></th>
464 </tr>
465 </thead>
466 <tbody class="bg-white divide-y divide-gray-200">
467 <?php if (!empty($payment_report['payments'])) : ?>
468 <?php foreach ($payment_report['payments'] as $payment): ?>
469 <tr class="hover:bg-gray-50">
470 <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
471 <?php echo esc_html(gmdate('M d, Y', strtotime($payment['date']))); ?>
472 </td>
473 <td class="px-4 py-3 whitespace-nowrap">
474 <?php if (!empty($payment['invoice_id'])) : ?>
475 <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-builder&invoice_id=' . (int) $payment['invoice_id'])); ?>" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
476 <?php echo esc_html($payment['invoice_number']); ?>
477 </a>
478 <?php else : ?>
479 <span class="text-sm text-gray-500"><?php echo esc_html($payment['invoice_number'] ?: ''); ?></span>
480 <?php endif; ?>
481 </td>
482 <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
483 <?php echo esc_html(easy_invoice_format_money($payment['amount'], null, (string) ($payment['currency'] ?? ''))); ?>
484 </td>
485 <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
486 <?php echo esc_html(ucfirst($payment['payment_method'])); ?>
487 </td>
488 <td class="px-4 py-3 whitespace-nowrap">
489 <?php
490 $status_class = 'bg-gray-100 text-gray-800';
491 if (in_array($payment['status'], ['completed', 'paid'])) {
492 $status_class = 'bg-green-100 text-green-800';
493 } elseif (in_array($payment['status'], ['pending', 'pending-bank', 'pending-cheque'])) {
494 $status_class = 'bg-yellow-100 text-yellow-800';
495 } elseif (in_array($payment['status'], ['failed', 'rejected'])) {
496 $status_class = 'bg-red-100 text-red-800';
497 }
498 ?>
499 <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <?php echo esc_attr($status_class); ?>">
500 <?php echo esc_html(ucfirst($payment['status'])); ?>
501 </span>
502 </td>
503 <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
504 <?php echo esc_html($payment['transaction_id'] ?: esc_html__('', 'easy-invoice')); ?>
505 </td>
506 <td class="px-4 py-3 whitespace-nowrap text-sm font-medium">
507 <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-payments&action=view&id=' . $payment['id'])); ?>" class="text-indigo-600 hover:text-indigo-900">View</a>
508 </td>
509 </tr>
510 <?php endforeach; ?>
511 <?php else: ?>
512 <tr>
513 <td colspan="7" class="px-4 py-4 text-center text-gray-500"><?php esc_html_e('No payment records found for the selected period', 'easy-invoice'); ?></td>
514 </tr>
515 <?php endif; ?>
516 </tbody>
517 </table>
518 </div>
519 </div>
520 </div>
521
522 <div id="invoice-report-content" class="tab-content hidden" style="display: none;">
523 <div class="p-4 bg-gray-50 border-b">
524 <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
525 <div class="bg-white p-4 rounded-md shadow-sm">
526 <div class="text-sm text-gray-500"><?php esc_html_e('Total Invoices', 'easy-invoice'); ?></div>
527 <div class="text-xl font-bold"><?php echo esc_html($invoice_report['count']); ?></div>
528 </div>
529 <div class="bg-white p-4 rounded-md shadow-sm">
530 <div class="text-sm text-gray-500"><?php esc_html_e('Total Amount', 'easy-invoice'); ?></div>
531 <div class="text-lg font-bold">
532 <?php
533 // Display total amounts by currency
534 if (isset($invoice_report['total_amounts_by_currency']) && !empty($invoice_report['total_amounts_by_currency'])): ?>
535 <?php foreach ($invoice_report['total_amounts_by_currency'] as $currency => $currency_data): ?>
536 <div class="text-sm">
537 <?php echo esc_html(easy_invoice_format_money($currency_data['amount'], null, (string) $currency)); ?>
538 <span class="text-xs text-gray-500">
539 <?php
540 $display_currency = $currency;
541 if ($display_currency === 'GLOBAL') {
542 $display_currency = get_option('easy_invoice_currency_code', 'USD');
543 }
544 echo esc_html(strtoupper($display_currency));
545 ?>
546 </span>
547 </div>
548 <?php endforeach; ?>
549 <?php else: ?>
550 $0.00
551 <?php endif; ?>
552 </div>
553 </div>
554 <div class="bg-white p-4 rounded-md shadow-sm">
555 <div class="text-sm text-gray-500"><?php esc_html_e('Average Invoice', 'easy-invoice'); ?></div>
556 <div class="text-lg font-bold">
557 <?php
558 // Calculate average amounts by currency
559 if (isset($invoice_report['total_amounts_by_currency']) && !empty($invoice_report['total_amounts_by_currency']) && $invoice_report['count'] > 0): ?>
560 <?php foreach ($invoice_report['total_amounts_by_currency'] as $currency => $currency_data): ?>
561 <div class="text-sm">
562 <?php
563 $avg_amount = $currency_data['amount'] / $invoice_report['count'];
564 echo esc_html(easy_invoice_format_money($avg_amount, null, (string) $currency));
565 ?>
566 <span class="text-xs text-gray-500">
567 <?php
568 $display_currency = $currency;
569 if ($display_currency === 'GLOBAL') {
570 $display_currency = get_option('easy_invoice_currency_code', 'USD');
571 }
572 echo esc_html(strtoupper($display_currency));
573 ?>
574 </span>
575 </div>
576 <?php endforeach; ?>
577 <?php else: ?>
578 $0.00
579 <?php endif; ?>
580 </div>
581 </div>
582 </div>
583 </div>
584
585 <div class="p-4">
586 <h3 class="text-md font-medium text-gray-700 mb-3"><?php esc_html_e('Invoice Status Distribution', 'easy-invoice'); ?></h3>
587 <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
588 <div class="bg-white p-4 rounded-md shadow-sm">
589 <canvas id="invoice-status-chart" height="200"></canvas>
590 </div>
591 <div class="bg-white p-4 rounded-md shadow-sm">
592 <table class="min-w-full divide-y divide-gray-200">
593 <thead>
594 <tr>
595 <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Status', 'easy-invoice'); ?></th>
596 <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Count', 'easy-invoice'); ?></th>
597 <th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Amount', 'easy-invoice'); ?></th>
598 </tr>
599 </thead>
600 <tbody>
601 <?php
602 $status_list = [
603 'paid' => __('Paid', 'easy-invoice'),
604 'partial' => __('Partially paid', 'easy-invoice'),
605 'unpaid' => __('Unpaid', 'easy-invoice'),
606 'overdue' => __('Overdue', 'easy-invoice'),
607 'draft' => __('Draft', 'easy-invoice'),
608 'canceled' => __('Cancelled', 'easy-invoice'),
609 'other' => __('Other', 'easy-invoice')
610 ];
611
612 foreach ($status_list as $status_key => $status_label) :
613 $status_data = isset($invoice_report['by_status'][$status_key]) ? $invoice_report['by_status'][$status_key] : ['amount' => 0, 'count' => 0];
614 ?>
615 <tr>
616 <td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
617 <?php echo esc_html($status_label); ?>
618 </td>
619 <td class="px-3 py-2 whitespace-nowrap text-sm text-right text-gray-500">
620 <?php echo esc_html($status_data['count']); ?>
621 </td>
622 <td class="px-3 py-2 whitespace-nowrap text-sm text-right text-gray-900">
623 <?php if (isset($status_data['amounts_by_currency']) && !empty($status_data['amounts_by_currency'])): ?>
624 <?php foreach ($status_data['amounts_by_currency'] as $currency => $currency_data): ?>
625 <div class="text-xs">
626 <?php echo esc_html(easy_invoice_format_money($currency_data['amount'], null, (string) $currency)); ?>
627 <span class="text-xs text-gray-500">
628 <?php
629 $display_currency = $currency;
630 if ($display_currency === 'GLOBAL') {
631 $display_currency = get_option('easy_invoice_currency_code', 'USD');
632 }
633 echo esc_html(strtoupper($display_currency));
634 ?>
635 </span>
636 </div>
637 <?php endforeach; ?>
638 <?php else: ?>
639 $0.00
640 <?php endif; ?>
641 </td>
642 </tr>
643 <?php endforeach; ?>
644 </tbody>
645 </table>
646 </div>
647 </div>
648 </div>
649
650 <div class="p-4">
651 <h3 class="text-md font-medium text-gray-700 mb-3"><?php esc_html_e('Recent Invoices', 'easy-invoice'); ?></h3>
652 <div class="overflow-x-auto">
653 <table class="min-w-full divide-y divide-gray-200">
654 <thead class="bg-gray-50">
655 <tr>
656 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Date', 'easy-invoice'); ?></th>
657 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Due Date', 'easy-invoice'); ?></th>
658 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Invoice #</th>
659 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Client', 'easy-invoice'); ?></th>
660 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Amount', 'easy-invoice'); ?></th>
661 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Status', 'easy-invoice'); ?></th>
662 <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Actions', 'easy-invoice'); ?></th>
663 </tr>
664 </thead>
665 <tbody class="bg-white divide-y divide-gray-200">
666 <?php if (!empty($invoice_report['invoices'])) : ?>
667 <?php foreach ($invoice_report['invoices'] as $invoice): ?>
668 <tr class="hover:bg-gray-50">
669 <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
670 <?php echo esc_html(gmdate('M d, Y', strtotime($invoice['issue_date']))); ?>
671 </td>
672 <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
673 <?php
674 if (!empty($invoice['due_date']) && $invoice['due_date'] !== '0000-00-00' && $invoice['due_date'] !== '1970-01-01') {
675 echo esc_html(gmdate('M d, Y', strtotime($invoice['due_date'])));
676 } else {
677 echo '-';
678 }
679 ?>
680 </td>
681 <td class="px-4 py-3 whitespace-nowrap">
682 <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-builder&invoice_id=' . (int) $invoice['id'])); ?>" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
683 <?php echo esc_html($invoice['invoice_number']); ?>
684 </a>
685 </td>
686 <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
687 <?php if (!empty($invoice['client_id']) && !empty($invoice['client_name'])): ?>
688 <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-clients&view=view&client_id=' . $invoice['client_id'])); ?>" class="text-gray-900 hover:text-indigo-900">
689 <?php echo esc_html($invoice['client_name']); ?>
690 </a>
691 <?php elseif (!empty($invoice['client_name'])): ?>
692 <span class="text-gray-900"><?php echo esc_html($invoice['client_name']); ?></span>
693 <?php else: ?>
694 <span class="text-gray-500">-</span>
695 <?php endif; ?>
696 </td>
697 <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
698 <?php
699 $display_currency = $invoice['currency'];
700 if ($display_currency === 'GLOBAL') {
701 $display_currency = get_option('easy_invoice_currency_code', 'USD');
702 }
703 echo esc_html(easy_invoice_format_money($invoice['total'], null, (string) $display_currency)); ?>
704 <span class="text-xs text-gray-500"><?php echo esc_html(strtoupper($display_currency)); ?></span>
705 </td>
706 <td class="px-4 py-3 whitespace-nowrap">
707 <?php
708 $status_class = 'bg-gray-100 text-gray-800';
709 if (in_array($invoice['status'], ['paid', 'completed'])) {
710 $status_class = 'bg-green-100 text-green-800';
711 } elseif ($invoice['status'] === 'unpaid') {
712 $status_class = 'bg-yellow-100 text-yellow-800';
713 } elseif ($invoice['status'] === 'partial') {
714 $status_class = 'bg-blue-100 text-blue-800';
715 } elseif ($invoice['status'] === 'overdue') {
716 $status_class = 'bg-red-100 text-red-800';
717 } elseif ($invoice['status'] === 'draft') {
718 $status_class = 'bg-gray-100 text-gray-800';
719 }
720 ?>
721 <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <?php echo esc_attr($status_class); ?>">
722 <?php echo esc_html($status_list[$invoice['status']] ?? ucfirst($invoice['status'])); ?>
723 </span>
724 </td>
725 <td class="px-4 py-3 whitespace-nowrap text-sm font-medium">
726 <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-builder&invoice_id=' . (int) $invoice['id'])); ?>" class="text-indigo-600 hover:text-indigo-900">View</a>
727 </td>
728 </tr>
729 <?php endforeach; ?>
730 <?php else: ?>
731 <tr>
732 <td colspan="7" class="px-4 py-4 text-center text-gray-500"><?php esc_html_e('No invoice records found for the selected period', 'easy-invoice'); ?></td>
733 </tr>
734 <?php endif; ?>
735 </tbody>
736 </table>
737 </div>
738 </div>
739 </div>
740 </div>
741 </div>
742
743 <?php
744 // Chart.js is enqueued as the 'chartjs' handle by ReportController::enqueueReportScripts()
745 // from assets/js/vendors/chart.min.js. It was previously also pulled from jsDelivr here,
746 // which both duplicated the local copy and broke the WordPress.org rule that all assets
747 // ship inside the plugin. The guard below already waits for Chart to be defined.
748 ?>
749 <script>
750
751
752 // Monthly revenue chart
753 function initializeRevenueChart() {
754 // Wait for Chart.js to be available
755 if (typeof Chart === 'undefined') {
756 setTimeout(initializeRevenueChart, 500);
757 return;
758 }
759
760 const chartElement = document.getElementById('revenue-chart');
761 if (chartElement) {
762 const monthlyRevenueLabels = <?php echo json_encode(array_keys($monthly_revenue)); ?>;
763
764
765
766 // Process multi-currency data for chart display
767 const monthlyRevenueData = <?php
768
769
770 // Get all unique currencies from monthly revenue data
771 $all_currencies = [];
772 if (is_array($monthly_revenue)) {
773 foreach ($monthly_revenue as $month => $currencies) {
774 if (is_array($currencies)) {
775 foreach ($currencies as $currency => $data) {
776 if (!in_array($currency, $all_currencies)) {
777 $all_currencies[] = $currency;
778 }
779 }
780 }
781 }
782 }
783 sort($all_currencies); // Sort currencies for consistent order
784
785 // Create datasets for each currency
786 $datasets = [];
787 $colors = [
788 'rgba(79, 70, 229, 0.8)', // Indigo for USD
789 'rgba(16, 185, 129, 0.8)', // Green for EUR
790 'rgba(245, 158, 11, 0.8)', // Yellow for other currencies
791 'rgba(239, 68, 68, 0.8)', // Red
792 'rgba(107, 114, 128, 0.8)' // Gray
793 ];
794
795 if (!empty($all_currencies)) {
796 foreach ($all_currencies as $index => $currency) {
797 $currency_data = [];
798 $symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency);
799
800 foreach ($monthly_revenue as $month => $currencies) {
801 $amount = isset($currencies[$currency]) ? $currencies[$currency]['amount'] : 0;
802 $currency_data[] = $amount;
803 }
804
805 $datasets[] = [
806 'label' => $currency . ' (' . $symbol . ')',
807 'data' => $currency_data,
808 'backgroundColor' => $colors[$index % count($colors)],
809 'borderColor' => easy_invoice_str_replace('0.8', '1', $colors[$index % count($colors)]),
810 'borderWidth' => 1
811 ];
812 }
813 }
814
815 echo json_encode($datasets);
816 ?>;
817
818
819
820 // Check if we have data to display
821 if (monthlyRevenueLabels.length > 0 && monthlyRevenueData.length > 0) {
822 const revenueCtx = document.getElementById('revenue-chart').getContext('2d');
823
824 // Destroy existing chart if it exists
825 if (window.revenueChart) {
826 window.revenueChart.destroy();
827 }
828
829 try {
830 window.revenueChart = new Chart(revenueCtx, {
831 type: 'bar',
832 data: {
833 labels: monthlyRevenueLabels,
834 datasets: monthlyRevenueData
835 },
836 options: {
837 responsive: true,
838 maintainAspectRatio: false,
839 scales: {
840 y: {
841 beginAtZero: true,
842 ticks: {
843 callback: function(value) {
844 return '$' + value.toLocaleString();
845 }
846 }
847 }
848 },
849 plugins: {
850 tooltip: {
851 callbacks: {
852 label: function(context) {
853 return context.dataset.label + ': ' + context.raw.toLocaleString();
854 }
855 }
856 },
857 legend: {
858 display: true,
859 position: 'top'
860 }
861 }
862 }
863 });
864 } catch (error) {
865 console.error('Error initializing Monthly Revenue Chart:', error);
866 const chartContainer = document.getElementById('revenue-chart');
867 if (chartContainer) {
868 chartContainer.style.display = 'flex';
869 chartContainer.style.alignItems = 'center';
870 chartContainer.style.justifyContent = 'center';
871 chartContainer.innerHTML = '<div class="text-red-500 text-center"><p><?php esc_html_e('Error loading chart. Please refresh the page.', 'easy-invoice'); ?></p></div>';
872 }
873 }
874 } else {
875 const chartContainer = document.getElementById('revenue-chart');
876 if (chartContainer) {
877 chartContainer.style.display = 'flex';
878 chartContainer.style.alignItems = 'center';
879 chartContainer.style.justifyContent = 'center';
880 chartContainer.innerHTML = '<div class="text-gray-500 text-center"><p><?php esc_html_e('No revenue data available for the selected period.', 'easy-invoice'); ?></p><p class="text-sm mt-2"><?php esc_html_e('Revenue data will appear here once payments are recorded.', 'easy-invoice'); ?></p></div>';
881 }
882 return;
883 }
884 }
885 }
886
887 // Initialize chart when DOM is ready
888 document.addEventListener('DOMContentLoaded', function() {
889 try {
890 // Make sure payment report tab is visible by default
891 const paymentReportTab = document.getElementById('payment-report-content');
892 const invoiceReportTab = document.getElementById('invoice-report-content');
893
894 if (paymentReportTab && invoiceReportTab) {
895 // Set proper visibility immediately
896 paymentReportTab.classList.remove('hidden');
897 paymentReportTab.classList.add('block');
898 paymentReportTab.style.display = 'block';
899
900 invoiceReportTab.classList.add('hidden');
901 invoiceReportTab.classList.remove('block');
902 invoiceReportTab.style.display = 'none';
903
904 // Set proper button state
905 const paymentButton = document.getElementById('tab-payment-report');
906 const invoiceButton = document.getElementById('tab-invoice-report');
907
908 if (paymentButton && invoiceButton) {
909 paymentButton.classList.add('active-tab', 'border-indigo-500', 'text-indigo-600');
910 paymentButton.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
911
912 invoiceButton.classList.remove('active-tab', 'border-indigo-500', 'text-indigo-600');
913 invoiceButton.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
914 }
915
916 // Also set the visibility with a delay to override any other scripts
917 setTimeout(function() {
918 paymentReportTab.classList.remove('hidden');
919 paymentReportTab.classList.add('block');
920 paymentReportTab.style.display = 'block';
921
922 invoiceReportTab.classList.add('hidden');
923 invoiceReportTab.classList.remove('block');
924 invoiceReportTab.style.display = 'none';
925 }, 100);
926 }
927
928 // Initialize the revenue chart
929 initializeRevenueChart();
930
931
932
933 // Invoice status pie chart
934 if (document.getElementById('status-chart')) {
935
936 const statusLabels = <?php
937 $status_labels = [];
938 $status_data = [];
939 $status_rgba = [];
940 // Colours keyed by status so a slice keeps its colour whichever statuses are present.
941 $ei_overview_colors = [
942 'paid' => 'rgba(34, 197, 94, 0.8)',
943 'partial' => 'rgba(59, 130, 246, 0.8)',
944 'unpaid' => 'rgba(234, 179, 8, 0.8)',
945 'overdue' => 'rgba(239, 68, 68, 0.8)',
946 'draft' => 'rgba(209, 213, 219, 0.8)',
947 'canceled' => 'rgba(107, 114, 128, 0.8)',
948 ];
949 if (isset($invoice_status['percentages']) && is_array($invoice_status['percentages'])) {
950 foreach ($invoice_status['percentages'] as $status => $percentage) {
951 $status_labels[] = $ei_status_names[$status] ?? ucfirst($status);
952 $status_data[] = $percentage;
953 $status_rgba[] = $ei_overview_colors[$status] ?? 'rgba(156, 163, 175, 0.8)';
954 }
955 }
956 echo json_encode($status_labels);
957 ?>;
958
959 const statusData = <?php
960 echo json_encode($status_data);
961 ?>;
962
963 const statusColors = <?php echo json_encode($status_rgba); ?>;
964
965 try {
966 if (statusLabels.length === 0 || statusData.length === 0) {
967 return;
968 }
969
970 const statusCtx = document.getElementById('status-chart').getContext('2d');
971
972 new Chart(statusCtx, {
973 type: 'doughnut',
974 data: {
975 labels: statusLabels,
976 datasets: [{
977 data: statusData,
978 backgroundColor: statusColors,
979 borderWidth: 1
980 }]
981 },
982 options: {
983 responsive: true,
984 maintainAspectRatio: false,
985 plugins: {
986 legend: {
987 display: false
988 },
989 tooltip: {
990 callbacks: {
991 label: function(context) {
992 return context.label + ': ' + context.raw + '%';
993 }
994 }
995 }
996 },
997 cutout: '70%'
998 }
999 });
1000 } catch (error) {
1001 console.error('Error creating status chart:', error);
1002 }
1003 }
1004
1005 // Payment methods chart - use try-catch to prevent errors from breaking the page
1006 try {
1007 if (document.getElementById('payment-methods-chart')) {
1008 const paymentMethodsData = <?php
1009 $methods = [];
1010 $counts = [];
1011 $methodColors = [];
1012 $colorPalette = ['rgba(79, 70, 229, 0.8)', 'rgba(59, 130, 246, 0.8)', 'rgba(16, 185, 129, 0.8)',
1013 'rgba(245, 158, 11, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(107, 114, 128, 0.8)'];
1014
1015 $i = 0;
1016 if (!empty($payment_report['by_method'])) {
1017 foreach ($payment_report['by_method'] as $method => $data) {
1018 $methods[] = ucfirst($method);
1019 $counts[] = $data['count'];
1020 $methodColors[] = $colorPalette[$i % count($colorPalette)];
1021 $i++;
1022 }
1023 }
1024
1025 echo json_encode([
1026 'labels' => $methods,
1027 'data' => $counts,
1028 'colors' => $methodColors
1029 ]);
1030 ?>;
1031
1032 if (paymentMethodsData.labels && paymentMethodsData.labels.length > 0) {
1033 const paymentMethodsCtx = document.getElementById('payment-methods-chart').getContext('2d');
1034 new Chart(paymentMethodsCtx, {
1035 type: 'pie',
1036 data: {
1037 labels: paymentMethodsData.labels,
1038 datasets: [{
1039 data: paymentMethodsData.data,
1040 backgroundColor: paymentMethodsData.colors,
1041 borderWidth: 1
1042 }]
1043 },
1044 options: {
1045 responsive: true,
1046 maintainAspectRatio: false,
1047 plugins: {
1048 legend: {
1049 position: 'right'
1050 },
1051 tooltip: {
1052 callbacks: {
1053 label: function(context) {
1054 const total = context.dataset.data.reduce((a, b) => a + b, 0);
1055 const percentage = Math.round((context.raw / total) * 100);
1056 return context.label + ': ' + context.raw + ' (' + percentage + '%)';
1057 }
1058 }
1059 }
1060 }
1061 }
1062 });
1063 } else {
1064 // If no data, display a message
1065 const canvas = document.getElementById('payment-methods-chart');
1066 const ctx = canvas.getContext('2d');
1067 ctx.font = '14px Arial';
1068 ctx.fillStyle = '#6B7280';
1069 ctx.textAlign = 'center';
1070 ctx.fillText('No payment method data available', canvas.width / 2, canvas.height / 2);
1071 }
1072 }
1073 } catch (error) {
1074 console.error('Error initializing payment methods chart:', error);
1075 }
1076
1077 // Invoice status detailed chart - use try-catch to prevent errors from breaking the page
1078 try {
1079 if (document.getElementById('invoice-status-chart')) {
1080 const invoiceStatusData = <?php
1081 $statuses = [];
1082 $statusCounts = [];
1083 // Colours keyed by status so a slice keeps its colour whichever statuses are present.
1084 $statusChartColors = [
1085 'paid' => 'rgba(34, 197, 94, 0.8)',
1086 'partial' => 'rgba(59, 130, 246, 0.8)',
1087 'unpaid' => 'rgba(234, 179, 8, 0.8)',
1088 'overdue' => 'rgba(239, 68, 68, 0.8)',
1089 'draft' => 'rgba(209, 213, 219, 0.8)',
1090 'canceled' => 'rgba(107, 114, 128, 0.8)',
1091 'other' => 'rgba(156, 163, 175, 0.8)',
1092 ];
1093 $statusColors = [];
1094 foreach ($status_list as $status_key => $status_label) {
1095 $status_data = $invoice_report['by_status'][$status_key] ?? ['count' => 0];
1096 if ($status_data['count'] > 0) {
1097 $statuses[] = $status_label;
1098 $statusCounts[] = $status_data['count'];
1099 $statusColors[] = $statusChartColors[$status_key] ?? 'rgba(156, 163, 175, 0.8)';
1100 }
1101 }
1102
1103 echo json_encode([
1104 'labels' => $statuses,
1105 'data' => $statusCounts,
1106 'colors' => $statusColors
1107 ]);
1108 ?>;
1109
1110 if (invoiceStatusData.labels && invoiceStatusData.labels.length > 0) {
1111 const invoiceStatusCtx = document.getElementById('invoice-status-chart').getContext('2d');
1112 window.easyInvoiceStatusChart = new Chart(invoiceStatusCtx, {
1113 type: 'pie',
1114 data: {
1115 labels: invoiceStatusData.labels,
1116 datasets: [{
1117 data: invoiceStatusData.data,
1118 backgroundColor: invoiceStatusData.colors,
1119 borderWidth: 1
1120 }]
1121 },
1122 options: {
1123 responsive: true,
1124 maintainAspectRatio: false,
1125 plugins: {
1126 legend: {
1127 position: 'right'
1128 },
1129 tooltip: {
1130 callbacks: {
1131 label: function(context) {
1132 const total = context.dataset.data.reduce((a, b) => a + b, 0);
1133 const percentage = Math.round((context.raw / total) * 100);
1134 return context.label + ': ' + context.raw + ' (' + percentage + '%)';
1135 }
1136 }
1137 }
1138 }
1139 }
1140 });
1141 } else {
1142 // If no data, display a message
1143 const canvas = document.getElementById('invoice-status-chart');
1144 const ctx = canvas.getContext('2d');
1145 ctx.font = '14px Arial';
1146 ctx.fillStyle = '#6B7280';
1147 ctx.textAlign = 'center';
1148 ctx.fillText('No invoice status data available', canvas.width / 2, canvas.height / 2);
1149 }
1150 }
1151 } catch (error) {
1152 console.error('Error initializing invoice status chart:', error);
1153 }
1154
1155 // Tab switching functionality
1156 const tabButtons = document.querySelectorAll('.tab-button');
1157
1158 tabButtons.forEach(button => {
1159 button.addEventListener('click', function(e) {
1160 // Prevent default behavior that might cause page reload
1161 e.preventDefault();
1162
1163 // Get button ID safely
1164 const buttonId = this.id || '';
1165
1166 // Store the target tab ID before modifying classes
1167 const targetId = buttonId.replace('tab-', '') + '-content';
1168
1169 // For troubleshooting
1170
1171
1172 // Remove active class from all buttons first
1173 tabButtons.forEach(btn => {
1174 btn.classList.remove('active-tab', 'border-indigo-500', 'text-indigo-600');
1175 btn.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
1176 });
1177
1178 // Add active class to clicked button
1179 this.classList.add('active-tab', 'border-indigo-500', 'text-indigo-600');
1180 this.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
1181
1182 // Get all tab contents
1183 const paymentReportTab = document.getElementById('payment-report-content');
1184 const invoiceReportTab = document.getElementById('invoice-report-content');
1185
1186 // Hide both tabs first
1187 if (paymentReportTab) {
1188 paymentReportTab.classList.add('hidden');
1189 paymentReportTab.classList.remove('block');
1190 paymentReportTab.style.display = 'none';
1191 }
1192
1193 if (invoiceReportTab) {
1194 invoiceReportTab.classList.add('hidden');
1195 invoiceReportTab.classList.remove('block');
1196 invoiceReportTab.style.display = 'none';
1197 }
1198
1199 // Show the target tab
1200 const targetTab = document.getElementById(targetId);
1201 if (targetTab) {
1202 // Short delay helps ensure DOM updates properly
1203 setTimeout(() => {
1204 targetTab.classList.remove('hidden');
1205 targetTab.classList.add('block');
1206 targetTab.style.display = 'block';
1207 // The chart was built while its tab was display:none (zero size); size it now.
1208 if (targetId === 'invoice-report-content' && window.easyInvoiceStatusChart) {
1209 window.easyInvoiceStatusChart.resize();
1210 }
1211 }, 10);
1212 } else {
1213 // Fallback to payment report if target not found
1214 if (paymentReportTab) {
1215 setTimeout(() => {
1216 paymentReportTab.classList.remove('hidden');
1217 paymentReportTab.classList.add('block');
1218 paymentReportTab.style.display = 'block';
1219 }, 10);
1220 }
1221 }
1222 });
1223 });
1224
1225 // Export report functionality (disabled to prevent conflict with detailed export)
1226 /*
1227 if (document.getElementById('export-report')) {
1228 document.getElementById('export-report').addEventListener('click', function() {
1229 try {
1230 // Create a timestamp for the filename
1231 const date = new Date();
1232 const timestamp = date.getFullYear() + '-' +
1233 String(date.getMonth() + 1).padStart(2, '0') + '-' +
1234 String(date.getDate()).padStart(2, '0');
1235
1236 // Generate CSV content
1237 let csvContent = 'data:text/csv;charset=utf-8,';
1238
1239 // Add report title and date range
1240 csvContent += '<?php echo esc_html__('Easy Invoice Report: Reports', 'easy-invoice'); ?>\r\n';
1241 csvContent += 'Date Range: ' + (document.getElementById('start_date') ? document.getElementById('start_date').value : 'N/A') + ' to ' + (document.getElementById('end_date') ? document.getElementById('end_date').value : 'N/A') + '\r\n\r\n';
1242
1243 // Add summary stats
1244 csvContent += 'Summary Statistics\r\n';
1245 <?php if (isset($summary_stats['total_revenue']) && is_array($summary_stats['total_revenue']) && !empty($summary_stats['total_revenue'])): ?>
1246 <?php foreach ($summary_stats['total_revenue'] as $currency => $data): ?>
1247 csvContent += 'Total Revenue (<?php echo esc_js($currency); ?>),<?php echo esc_js($data['symbol'] . number_format($data['amount'], 2)); ?>\r\n';
1248 <?php endforeach; ?>
1249 <?php else: ?>
1250 csvContent += 'Total Revenue,$0.00\r\n';
1251 <?php endif; ?>
1252 csvContent += 'Total Invoices,<?php echo esc_js(isset($summary_stats['total_invoices']) ? $summary_stats['total_invoices'] : 0); ?>\r\n';
1253 csvContent += 'Active Clients,<?php echo esc_js(isset($summary_stats['active_clients']) ? $summary_stats['active_clients'] : 0); ?>\r\n';
1254 csvContent += 'Average Payment Time,<?php echo esc_js(isset($summary_stats['avg_payment_time']) ? $summary_stats['avg_payment_time'] : 0); ?> days\r\n\r\n';
1255
1256 // Add monthly revenue
1257 csvContent += 'Monthly Revenue\r\n';
1258 csvContent += 'Month,USD,EUR,Other Currencies\r\n';
1259 <?php if (isset($monthly_revenue) && is_array($monthly_revenue)): ?>
1260 <?php foreach ($monthly_revenue as $month => $currencies): ?>
1261 <?php
1262 $usd_amount = isset($currencies['USD']) ? $currencies['USD']['amount'] : 0;
1263 $eur_amount = isset($currencies['EUR']) ? $currencies['EUR']['amount'] : 0;
1264 $other_currencies = [];
1265 if (is_array($currencies)) {
1266 foreach ($currencies as $currency => $data) {
1267 if ($currency !== 'USD' && $currency !== 'EUR' && is_array($data)) {
1268 $other_currencies[] = $currency . ': ' . $data['symbol'] . number_format($data['amount'], 2);
1269 }
1270 }
1271 }
1272 $other_text = !empty($other_currencies) ? implode('; ', $other_currencies) : '';
1273 ?>
1274 csvContent += '<?php echo esc_js($month); ?>,$<?php echo esc_js(number_format($usd_amount, 2)); ?>,€<?php echo esc_js(number_format($eur_amount, 2)); ?>,"<?php echo esc_js($other_text); ?>"\r\n';
1275 <?php endforeach; ?>
1276 <?php endif; ?>
1277 csvContent += '\r\n';
1278
1279 // Add invoice status
1280 csvContent += 'Invoice Status\r\n';
1281 csvContent += 'Status,Percentage,Count\r\n';
1282 <?php if (isset($invoice_status['percentages']) && is_array($invoice_status['percentages'])): ?>
1283 <?php foreach ($invoice_status['percentages'] as $status => $percentage): ?>
1284 <?php $count = isset($invoice_status['counts'][$status]) ? $invoice_status['counts'][$status] : 0; ?>
1285 csvContent += '<?php echo esc_js(ucfirst($status)); ?>,<?php echo esc_js($percentage); ?>%,<?php echo esc_js($count); ?>\r\n';
1286 <?php endforeach; ?>
1287 <?php endif; ?>
1288 csvContent += '\r\n';
1289
1290 // Add top clients
1291 csvContent += 'Top Clients by Revenue\r\n';
1292 csvContent += 'Client,Email,Total Amount (USD),Total Amount (EUR),Other Currencies,Invoices,Last Invoice\r\n';
1293 <?php if (isset($top_clients) && is_array($top_clients)): ?>
1294 <?php foreach ($top_clients as $client): ?>
1295 <?php
1296 $usd_amount = isset($client['total_amount']['USD']) ? $client['total_amount']['USD']['amount'] : 0;
1297 $eur_amount = isset($client['total_amount']['EUR']) ? $client['total_amount']['EUR']['amount'] : 0;
1298 $other_currencies = [];
1299 if (isset($client['total_amount']) && is_array($client['total_amount'])) {
1300 foreach ($client['total_amount'] as $currency => $data) {
1301 if ($currency !== 'USD' && $currency !== 'EUR' && is_array($data)) {
1302 $other_currencies[] = $currency . ': ' . $data['symbol'] . number_format($data['amount'], 2);
1303 }
1304 }
1305 }
1306 $other_text = !empty($other_currencies) ? implode('; ', $other_currencies) : '';
1307 $client_name = isset($client['name']) ? $client['name'] : 'Unknown';
1308 $client_email = isset($client['email']) ? $client['email'] : '';
1309 $total_invoices = isset($client['total_invoices']) ? $client['total_invoices'] : 0;
1310 $last_invoice = isset($client['last_invoice']) && !empty($client['last_invoice']) ? gmdate('Y-m-d', strtotime($client['last_invoice'])) : 'N/A';
1311 ?>
1312 csvContent += '<?php echo esc_js($client_name); ?>,<?php echo esc_js($client_email); ?>,$<?php echo esc_js(number_format($usd_amount, 2)); ?>,€<?php echo esc_js(number_format($eur_amount, 2)); ?>,"<?php echo esc_js($other_text); ?>",<?php echo esc_js($total_invoices); ?>,<?php echo esc_js($last_invoice); ?>\r\n';
1313 <?php endforeach; ?>
1314 <?php endif; ?>
1315
1316 // Create download link
1317 const encodedUri = encodeURI(csvContent);
1318 const link = document.createElement('a');
1319 link.setAttribute('href', encodedUri);
1320 link.setAttribute('download', 'easy-invoice-report-' + timestamp + '.csv');
1321 document.body.appendChild(link);
1322
1323 // Trigger download
1324 link.click();
1325 document.body.removeChild(link);
1326 } catch (error) {
1327 alert('Error generating export. Please try again.');
1328 }
1329 });
1330 }
1331 */
1332
1333 // Note: The above export functionality is disabled to prevent double downloads
1334 // with the detailed export button below. Only the detailed export is active.
1335
1336 // Re-enable the proper export functionality with a different approach
1337 if (document.getElementById('export-report')) {
1338 document.getElementById('export-report').addEventListener('click', function() {
1339 // Set a flag to prevent detailed export from running
1340 window.exportInProgress = true;
1341
1342 // Clear the flag after a short delay
1343 setTimeout(() => {
1344 window.exportInProgress = false;
1345 }, 1000);
1346 try {
1347 // Create a timestamp for the filename
1348 const date = new Date();
1349 const timestamp = date.getFullYear() + '-' +
1350 String(date.getMonth() + 1).padStart(2, '0') + '-' +
1351 String(date.getDate()).padStart(2, '0');
1352
1353 // Generate CSV content
1354 let csvContent = 'data:text/csv;charset=utf-8,';
1355
1356 // Add report title and date range
1357 csvContent += 'Easy Invoice Report: Reports\r\n';
1358 csvContent += 'Date Range: ' + (document.getElementById('start_date') ? document.getElementById('start_date').value : 'N/A') + ' to ' + (document.getElementById('end_date') ? document.getElementById('end_date').value : 'N/A') + '\r\n\r\n';
1359
1360 // Add summary stats
1361 csvContent += 'Summary Statistics\r\n';
1362 csvContent += 'Total Revenue,$0.00\r\n';
1363 csvContent += 'Total Invoices,0\r\n';
1364 csvContent += 'Active Clients,0\r\n';
1365 csvContent += 'Average Payment Time,0 days\r\n\r\n';
1366
1367 // Add monthly revenue
1368 csvContent += 'Monthly Revenue\r\n';
1369 csvContent += 'Month,Revenue\r\n';
1370 csvContent += 'No data available\r\n\r\n';
1371
1372 // Add invoice status
1373 csvContent += 'Invoice Status\r\n';
1374 csvContent += 'Status,Percentage,Count\r\n';
1375 csvContent += 'No data available\r\n\r\n';
1376
1377 // Add top clients
1378 csvContent += 'Top Clients by Revenue\r\n';
1379 csvContent += 'Client,Email,Total Amount,Invoices,Last Invoice\r\n';
1380 csvContent += 'No data available\r\n';
1381
1382 // Create download link
1383 const encodedUri = encodeURI(csvContent);
1384 const link = document.createElement('a');
1385 link.setAttribute('href', encodedUri);
1386 link.setAttribute('download', 'easy-invoice-report-' + timestamp + '.csv');
1387 document.body.appendChild(link);
1388
1389 // Trigger download
1390 link.click();
1391 document.body.removeChild(link);
1392 } catch (error) {
1393 alert('Error generating export. Please try again.');
1394 }
1395 });
1396 }
1397
1398 // Simple Export functionality
1399 if (document.getElementById('export-detailed-report')) {
1400 // Remove any existing event listeners to prevent duplicates
1401 const exportButton = document.getElementById('export-detailed-report');
1402 const newButton = exportButton.cloneNode(true);
1403 exportButton.parentNode.replaceChild(newButton, exportButton);
1404
1405 newButton.addEventListener('click', function(e) {
1406 // Prevent multiple clicks
1407 e.preventDefault();
1408 e.stopPropagation();
1409
1410 // Check if another export is in progress
1411 if (window.exportInProgress) {
1412 return;
1413 }
1414
1415 // Disable button to prevent multiple clicks
1416 this.disabled = true;
1417 this.style.opacity = '0.5';
1418
1419 // Re-enable after 2 seconds
1420 setTimeout(() => {
1421 this.disabled = false;
1422 this.style.opacity = '1';
1423 }, 2000);
1424 try {
1425 // Get active tab
1426 const activeTab = document.querySelector('.tab-button.active-tab');
1427 if (!activeTab) {
1428 alert('Please select a report tab first.');
1429 return;
1430 }
1431
1432 // Determine report type
1433 const isPaymentTab = activeTab.id === 'tab-payment-report';
1434 const reportType = isPaymentTab ? 'payment' : 'invoice';
1435
1436 // Get data based on report type
1437 let data, headers, fileName;
1438
1439 if (isPaymentTab) {
1440 data = <?php echo json_encode(isset($payment_report['payments']) ? $payment_report['payments'] : []); ?>;
1441 headers = ['Date', 'Invoice', 'Amount', 'Currency', 'Method', 'Status', 'Transaction ID'];
1442 fileName = 'easy-invoice-payment-report-' + new Date().toISOString().split('T')[0] + '.csv';
1443 } else {
1444 data = <?php echo json_encode(isset($invoice_report['invoices']) ? $invoice_report['invoices'] : []); ?>;
1445 headers = ['Date', 'Due Date', 'Invoice #', 'Client', 'Amount', 'Currency', 'Status'];
1446 fileName = 'easy-invoice-invoice-report-' + new Date().toISOString().split('T')[0] + '.csv';
1447 }
1448
1449 // Generate CSV with proper escaping
1450 let csv = headers.map(header => `"${header}"`).join(',') + '\r\n';
1451
1452 if (Array.isArray(data) && data.length > 0) {
1453 data.forEach(function(item, index) {
1454 if (isPaymentTab) {
1455 // Payment data
1456 const row = [
1457 item.date ? new Date(item.date).toLocaleDateString() : 'N/A',
1458 item.invoice_number || 'N/A',
1459 (item.currency_symbol || '$') + (parseFloat(item.amount || 0).toFixed(2)),
1460 item.currency || 'USD',
1461 item.payment_method || 'Unknown',
1462 item.status || 'Unknown',
1463 item.transaction_id || '-'
1464 ];
1465 csv += row.map(field => `"${field}"`).join(',') + '\r\n';
1466 } else {
1467 // Invoice data
1468 const row = [
1469 item.issue_date ? new Date(item.issue_date).toLocaleDateString() : 'N/A',
1470 item.due_date ? new Date(item.due_date).toLocaleDateString() : '-',
1471 item.invoice_number || 'N/A',
1472 item.client_name || 'Unknown',
1473 (item.currency_symbol || '$') + (parseFloat(item.total || 0).toFixed(2)),
1474 item.currency || 'USD',
1475 item.status || 'Unknown'
1476 ];
1477 csv += row.map(field => `"${field}"`).join(',') + '\r\n';
1478 }
1479 });
1480 } else {
1481 csv += 'No data available\r\n';
1482 }
1483
1484 // Create download using data URL approach
1485 const csvContent = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);
1486 const link = document.createElement('a');
1487 link.href = csvContent;
1488 link.download = fileName;
1489 link.style.display = 'none';
1490 document.body.appendChild(link);
1491
1492 // Trigger download
1493 link.click();
1494 document.body.removeChild(link);
1495
1496 } catch (error) {
1497 console.error('Export error:', error);
1498 alert('Error generating export. Please try again.');
1499 }
1500 });
1501 }
1502 } catch (error) {
1503 console.error('Error in reports page initialization:', error);
1504
1505 // Make sure tabs are still visible even if there's an error
1506 const tabContents = document.querySelectorAll('.tab-content');
1507 if (tabContents.length > 0) {
1508 tabContents[0].classList.remove('hidden');
1509 tabContents[0].classList.add('block');
1510 }
1511 }
1512 });
1513 </script>
1514