PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / trunk
Easy Invoice – Invoice Generator, PDF Quotes & Payments vtrunk
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 2.1.3 2.1.4 All 55 releases
easy-invoice / assets / js / reports.js

reports.js in Easy Invoice – Invoice Generator, PDF Quotes & Payments trunk, at assets/js/reports.js

654 lines 27.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Easy Invoice Reports JavaScript
3 *
4 * Handles all reporting functionality including tab switching,
5 * chart rendering, and export features for the reports page.
6 *
7 * @package EasyInvoice
8 * @since 1.0.0
9 */
10
11 (function($) {
12 'use strict';
13
14 const EasyInvoiceReports = {
15 /**
16 * Initialize the reporting functionality
17 */
18 init: function() {
19 this.ensurePaymentTabVisibility();
20 this.initCharts();
21 this.setupTabSwitching();
22 this.setupExportHandlers();
23 },
24
25 /**
26 * Ensure payment tab is properly displayed by default
27 */
28 ensurePaymentTabVisibility: function() {
29 const paymentReportTab = document.getElementById('payment-report-content');
30 const invoiceReportTab = document.getElementById('invoice-report-content');
31
32 if (paymentReportTab && invoiceReportTab) {
33 // Set proper visibility immediately
34 paymentReportTab.classList.remove('hidden');
35 paymentReportTab.classList.add('block');
36 paymentReportTab.style.display = 'block';
37
38 invoiceReportTab.classList.add('hidden');
39 invoiceReportTab.classList.remove('block');
40 invoiceReportTab.style.display = 'none';
41
42 // Set proper button state
43 const paymentButton = document.getElementById('tab-payment-report');
44 const invoiceButton = document.getElementById('tab-invoice-report');
45
46 if (paymentButton && invoiceButton) {
47 paymentButton.classList.add('active-tab', 'border-indigo-500', 'text-indigo-600');
48 paymentButton.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
49
50 invoiceButton.classList.remove('active-tab', 'border-indigo-500', 'text-indigo-600');
51 invoiceButton.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
52 }
53
54 // Also set the visibility with a delay to override any other scripts
55 setTimeout(function() {
56 paymentReportTab.classList.remove('hidden');
57 paymentReportTab.classList.add('block');
58 paymentReportTab.style.display = 'block !important';
59
60 invoiceReportTab.classList.add('hidden');
61 invoiceReportTab.classList.remove('block');
62 invoiceReportTab.style.display = 'none';
63 }, 100);
64 }
65 },
66
67 /**
68 * Initialize all chart visualizations
69 */
70 initCharts: function() {
71 this.initRevenueChart();
72 this.initStatusChart();
73 this.initPaymentMethodsChart();
74 this.initInvoiceStatusChart();
75 },
76
77 /**
78 * Initialize monthly revenue chart
79 */
80 initRevenueChart: function() {
81 try {
82 if (!document.getElementById('revenue-chart')) {
83 return;
84 }
85
86 const monthlyRevenue = easy_invoice_reports.monthly_revenue || {};
87 const monthlyRevenueLabels = Object.keys(monthlyRevenue);
88 const monthlyRevenueData = Object.values(monthlyRevenue);
89
90 const revenueCtx = document.getElementById('revenue-chart').getContext('2d');
91 new Chart(revenueCtx, {
92 type: 'bar',
93 data: {
94 labels: monthlyRevenueLabels,
95 datasets: [{
96 label: 'Revenue',
97 data: monthlyRevenueData,
98 backgroundColor: 'rgba(79, 70, 229, 0.2)',
99 borderColor: 'rgba(79, 70, 229, 1)',
100 borderWidth: 1
101 }]
102 },
103 options: {
104 responsive: true,
105 maintainAspectRatio: false,
106 scales: {
107 y: {
108 beginAtZero: true,
109 ticks: {
110 callback: function(value) {
111 return '$' + value.toLocaleString();
112 }
113 }
114 }
115 },
116 plugins: {
117 tooltip: {
118 callbacks: {
119 label: function(context) {
120 return '$' + context.raw.toLocaleString();
121 }
122 }
123 }
124 }
125 }
126 });
127 } catch (error) {
128 // Error initializing revenue chart
129 this.handleChartError('revenue-chart');
130 }
131 },
132
133 /**
134 * Initialize invoice status pie chart
135 */
136 initStatusChart: function() {
137 try {
138 if (!document.getElementById('status-chart')) {
139 return;
140 }
141
142 const invoiceStatus = easy_invoice_reports.invoice_status || {};
143
144 // Filter out the counts key
145 const statusLabels = Object.keys(invoiceStatus).filter(key => key !== 'counts');
146 const statusData = statusLabels.map(label => invoiceStatus[label]);
147
148 const statusColors = [
149 'rgba(34, 197, 94, 0.8)', // Green for Paid
150 'rgba(234, 179, 8, 0.8)', // Yellow for Unpaid
151 'rgba(239, 68, 68, 0.8)', // Red for Overdue
152 'rgba(209, 213, 219, 0.8)' // Gray for Draft
153 ];
154
155 const statusCtx = document.getElementById('status-chart').getContext('2d');
156 new Chart(statusCtx, {
157 type: 'doughnut',
158 data: {
159 labels: statusLabels,
160 datasets: [{
161 data: statusData,
162 backgroundColor: statusColors,
163 borderWidth: 1
164 }]
165 },
166 options: {
167 responsive: true,
168 maintainAspectRatio: false,
169 plugins: {
170 legend: {
171 display: false
172 },
173 tooltip: {
174 callbacks: {
175 label: function(context) {
176 return context.label + ': ' + context.raw + '%';
177 }
178 }
179 }
180 },
181 cutout: '70%'
182 }
183 });
184 } catch (error) {
185 // Error initializing status chart
186 this.handleChartError('status-chart');
187 }
188 },
189
190 /**
191 * Initialize payment methods chart
192 */
193 initPaymentMethodsChart: function() {
194 try {
195 if (!document.getElementById('payment-methods-chart')) {
196 return;
197 }
198
199 const paymentReport = easy_invoice_reports.payment_report || {};
200 const paymentMethods = paymentReport.by_method || {};
201
202 const methods = [];
203 const counts = [];
204 const methodColors = [];
205 const colorPalette = [
206 'rgba(79, 70, 229, 0.8)',
207 'rgba(59, 130, 246, 0.8)',
208 'rgba(16, 185, 129, 0.8)',
209 'rgba(245, 158, 11, 0.8)',
210 'rgba(239, 68, 68, 0.8)',
211 'rgba(107, 114, 128, 0.8)'
212 ];
213
214 let i = 0;
215 for (const method in paymentMethods) {
216 methods.push(this.capitalizeFirst(method));
217 counts.push(paymentMethods[method].count);
218 methodColors.push(colorPalette[i % colorPalette.length]);
219 i++;
220 }
221
222 if (methods.length > 0) {
223 const paymentMethodsCtx = document.getElementById('payment-methods-chart').getContext('2d');
224 new Chart(paymentMethodsCtx, {
225 type: 'pie',
226 data: {
227 labels: methods,
228 datasets: [{
229 data: counts,
230 backgroundColor: methodColors,
231 borderWidth: 1
232 }]
233 },
234 options: {
235 responsive: true,
236 maintainAspectRatio: false,
237 plugins: {
238 legend: {
239 position: 'right'
240 },
241 tooltip: {
242 callbacks: {
243 label: function(context) {
244 const total = context.dataset.data.reduce((a, b) => a + b, 0);
245 const percentage = Math.round((context.raw / total) * 100);
246 return context.label + ': ' + context.raw + ' (' + percentage + '%)';
247 }
248 }
249 }
250 }
251 }
252 });
253 } else {
254 this.handleChartError('payment-methods-chart', 'No payment method data available');
255 }
256 } catch (error) {
257 // Error initializing payment methods chart
258 this.handleChartError('payment-methods-chart');
259 }
260 },
261
262 /**
263 * Initialize invoice status detailed chart
264 */
265 initInvoiceStatusChart: function() {
266 try {
267 if (!document.getElementById('invoice-status-chart')) {
268 return;
269 }
270
271 const invoiceReport = easy_invoice_reports.invoice_report || {};
272 const statusData = invoiceReport.by_status || {};
273
274 const statusList = {
275 'paid': 'Paid',
276 'unpaid': 'Unpaid',
277 'overdue': 'Overdue',
278 'draft': 'Draft',
279 'canceled': 'Canceled',
280 'other': 'Other'
281 };
282
283 const statusChartColors = [
284 'rgba(34, 197, 94, 0.8)', // Green for Paid
285 'rgba(234, 179, 8, 0.8)', // Yellow for Unpaid
286 'rgba(239, 68, 68, 0.8)', // Red for Overdue
287 'rgba(209, 213, 219, 0.8)', // Gray for Draft
288 'rgba(107, 114, 128, 0.8)', // Dark Gray for Canceled
289 'rgba(156, 163, 175, 0.8)' // Medium Gray for Other
290 ];
291
292 const statuses = [];
293 const statusCounts = [];
294 const colors = [];
295
296 let i = 0;
297 for (const statusKey in statusList) {
298 const data = statusData[statusKey] || { count: 0 };
299 if (data.count > 0) {
300 statuses.push(statusList[statusKey]);
301 statusCounts.push(data.count);
302 colors.push(statusChartColors[i]);
303 }
304 i++;
305 }
306
307 if (statuses.length > 0) {
308 const invoiceStatusCtx = document.getElementById('invoice-status-chart').getContext('2d');
309 new Chart(invoiceStatusCtx, {
310 type: 'pie',
311 data: {
312 labels: statuses,
313 datasets: [{
314 data: statusCounts,
315 backgroundColor: colors,
316 borderWidth: 1
317 }]
318 },
319 options: {
320 responsive: true,
321 maintainAspectRatio: false,
322 plugins: {
323 legend: {
324 position: 'right'
325 },
326 tooltip: {
327 callbacks: {
328 label: function(context) {
329 const total = context.dataset.data.reduce((a, b) => a + b, 0);
330 const percentage = Math.round((context.raw / total) * 100);
331 return context.label + ': ' + context.raw + ' (' + percentage + '%)';
332 }
333 }
334 }
335 }
336 }
337 });
338 } else {
339 this.handleChartError('invoice-status-chart', 'No invoice status data available');
340 }
341 } catch (error) {
342 // Error initializing invoice status chart
343 this.handleChartError('invoice-status-chart');
344 }
345 },
346
347 /**
348 * Handle chart initialization errors
349 */
350 handleChartError: function(canvasId, message) {
351 const canvas = document.getElementById(canvasId);
352 if (!canvas) return;
353
354 const ctx = canvas.getContext('2d');
355 ctx.clearRect(0, 0, canvas.width, canvas.height);
356 ctx.font = '14px Arial';
357 ctx.fillStyle = '#6B7280';
358 ctx.textAlign = 'center';
359 ctx.fillText(message || 'Error loading chart data', canvas.width / 2, canvas.height / 2);
360 },
361
362 /**
363 * Setup tab switching functionality
364 */
365 setupTabSwitching: function() {
366 const self = this;
367 const tabButtons = document.querySelectorAll('.tab-button');
368
369 tabButtons.forEach(button => {
370 button.addEventListener('click', function(e) {
371 // Prevent default behavior that might cause page reload
372 e.preventDefault();
373
374 // Get button ID safely
375 const buttonId = this.id || '';
376
377 // Store the target tab ID before modifying classes
378 const targetId = buttonId.replace('tab-', '') + '-content';
379
380 // Remove active class from all buttons first
381 tabButtons.forEach(btn => {
382 btn.classList.remove('active-tab', 'border-indigo-500', 'text-indigo-600');
383 btn.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
384 });
385
386 // Add active class to clicked button
387 this.classList.add('active-tab', 'border-indigo-500', 'text-indigo-600');
388 this.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300');
389
390 // Get all tab contents
391 const paymentReportTab = document.getElementById('payment-report-content');
392 const invoiceReportTab = document.getElementById('invoice-report-content');
393
394 // Hide both tabs first
395 if (paymentReportTab) {
396 paymentReportTab.classList.add('hidden');
397 paymentReportTab.classList.remove('block');
398 paymentReportTab.style.display = 'none';
399 }
400
401 if (invoiceReportTab) {
402 invoiceReportTab.classList.add('hidden');
403 invoiceReportTab.classList.remove('block');
404 invoiceReportTab.style.display = 'none';
405 }
406
407 // Show the target tab
408 const targetTab = document.getElementById(targetId);
409 if (targetTab) {
410 // Short delay helps ensure DOM updates properly
411 setTimeout(() => {
412 targetTab.classList.remove('hidden');
413 targetTab.classList.add('block');
414 targetTab.style.display = 'block';
415 }, 10);
416 } else {
417 // Fallback to payment report if target not found
418 if (paymentReportTab) {
419 setTimeout(() => {
420 paymentReportTab.classList.remove('hidden');
421 paymentReportTab.classList.add('block');
422 paymentReportTab.style.display = 'block';
423 }, 10);
424 }
425 }
426 });
427 });
428 },
429
430 /**
431 * Setup report export functionality
432 */
433 setupExportHandlers: function() {
434 this.setupMainReportExport();
435 this.setupDetailedReportExport();
436 },
437
438 /**
439 * Setup main report export
440 */
441 setupMainReportExport: function() {
442 const self = this;
443 const exportButton = document.getElementById('export-report');
444
445 if (!exportButton) return;
446
447 exportButton.addEventListener('click', function() {
448 // Create a timestamp for the filename
449 const timestamp = self.getFormattedDate();
450
451 // Generate CSV content
452 let csvContent = 'data:text/csv;charset=utf-8,';
453
454 // Add report title and date range
455 const title = document.querySelector('h1') ? document.querySelector('h1').textContent : 'Reports';
456 csvContent += 'Easy Invoice Report: ' + title + '\r\n';
457 csvContent += 'Date Range: ' + easy_invoice_reports.start_date + ' to ' + easy_invoice_reports.end_date + '\r\n\r\n';
458
459 // Add summary stats from the global data
460 const stats = easy_invoice_reports.summary_stats || {};
461 csvContent += 'Summary Statistics\r\n';
462 csvContent += 'Total Revenue,$' + self.formatNumber(stats.total_revenue) + '\r\n';
463 csvContent += 'Total Invoices,' + stats.total_invoices + '\r\n';
464 csvContent += 'Active Clients,' + stats.active_clients + '\r\n';
465 csvContent += 'Average Payment Time,' + stats.avg_payment_time + ' days\r\n\r\n';
466
467 // Add monthly revenue
468 const monthlyRevenue = easy_invoice_reports.monthly_revenue || {};
469 csvContent += 'Monthly Revenue\r\n';
470 csvContent += 'Month,Revenue\r\n';
471
472 for (const month in monthlyRevenue) {
473 csvContent += month + ',$' + self.formatNumber(monthlyRevenue[month]) + '\r\n';
474 }
475 csvContent += '\r\n';
476
477 // Add invoice status
478 const invoiceStatus = easy_invoice_reports.invoice_status || {};
479 csvContent += 'Invoice Status\r\n';
480 csvContent += 'Status,Percentage,Count\r\n';
481
482 for (const status in invoiceStatus) {
483 if (status !== 'counts') {
484 const counts = invoiceStatus.counts || {};
485 const count = counts[status.toLowerCase()] || 0;
486 csvContent += status + ',' + invoiceStatus[status] + '%,' + count + '\r\n';
487 }
488 }
489 csvContent += '\r\n';
490
491 // Add top clients
492 const topClients = easy_invoice_reports.top_clients || [];
493 csvContent += 'Top Clients by Revenue\r\n';
494 csvContent += 'Client,Email,Total Amount,Invoices,Last Invoice\r\n';
495
496 topClients.forEach(function(client) {
497 const lastInvoice = client.last_invoice ? self.formatDate(client.last_invoice) : 'N/A';
498 csvContent += self.escapeCsvValue(client.name) + ',' +
499 self.escapeCsvValue(client.email) + ',' +
500 '$' + self.formatNumber(client.total_amount) + ',' +
501 client.total_invoices + ',' +
502 lastInvoice + '\r\n';
503 });
504
505 self.triggerDownload(csvContent, 'easy-invoice-report-' + timestamp + '.csv');
506 });
507 },
508
509 /**
510 * Setup detailed report export
511 */
512 setupDetailedReportExport: function() {
513 const self = this;
514 const exportButton = document.getElementById('export-detailed-report');
515
516 if (!exportButton) return;
517
518 exportButton.addEventListener('click', function() {
519 // Create a timestamp for the filename
520 const timestamp = self.getFormattedDate();
521
522 // Determine which tab is active
523 const activeTab = document.querySelector('.tab-button.active-tab');
524 if (!activeTab) return;
525
526 const activeTabId = activeTab.id;
527 let reportType = activeTabId === 'tab-payment-report' ? 'payment' : 'invoice';
528 let reportData, fileName, headers, rows = [];
529
530 if (reportType === 'payment') {
531 // Export payment report
532 const paymentReport = easy_invoice_reports.payment_report || {};
533 reportData = paymentReport.payments || [];
534 fileName = 'easy-invoice-payment-report-' + timestamp + '.csv';
535 headers = ['Date', 'Invoice', 'Amount', 'Method', 'Status', 'Transaction ID'];
536
537 // Generate rows
538 reportData.forEach(function(payment) {
539 const date = self.formatDate(payment.date);
540 const row = [
541 date,
542 payment.invoice_number,
543 payment.currency_symbol + self.formatNumber(payment.amount),
544 self.capitalizeFirst(payment.payment_method),
545 self.capitalizeFirst(payment.status),
546 payment.transaction_id || '-'
547 ];
548 rows.push(row);
549 });
550 } else {
551 // Export invoice report
552 const invoiceReport = easy_invoice_reports.invoice_report || {};
553 reportData = invoiceReport.invoices || [];
554 fileName = 'easy-invoice-invoice-report-' + timestamp + '.csv';
555 headers = ['Date', 'Due Date', 'Invoice #', 'Client', 'Amount', 'Status'];
556
557 // Generate rows
558 reportData.forEach(function(invoice) {
559 const issueDate = self.formatDate(invoice.issue_date);
560 const dueDate = invoice.due_date ? self.formatDate(invoice.due_date) : '-';
561 const row = [
562 issueDate,
563 dueDate,
564 invoice.invoice_number,
565 self.escapeCsvValue(invoice.client_name),
566 '$' + self.formatNumber(invoice.total),
567 self.capitalizeFirst(invoice.status)
568 ];
569 rows.push(row);
570 });
571 }
572
573 // Generate CSV content
574 let csvContent = 'data:text/csv;charset=utf-8,';
575 csvContent += headers.join(',') + '\r\n';
576
577 rows.forEach(function(row) {
578 csvContent += row.join(',') + '\r\n';
579 });
580
581 self.triggerDownload(csvContent, fileName);
582 });
583 },
584
585 /**
586 * Trigger CSV download
587 */
588 triggerDownload: function(csvContent, fileName) {
589 const encodedUri = encodeURI(csvContent);
590 const link = document.createElement('a');
591 link.setAttribute('href', encodedUri);
592 link.setAttribute('download', fileName);
593 document.body.appendChild(link);
594
595 // Trigger download
596 link.click();
597 document.body.removeChild(link);
598 },
599
600 /**
601 * Format date for display
602 */
603 formatDate: function(dateString) {
604 const date = new Date(dateString);
605 return date.toLocaleDateString();
606 },
607
608 /**
609 * Get formatted date for filenames
610 */
611 getFormattedDate: function() {
612 const date = new Date();
613 return date.getFullYear() + '-' +
614 String(date.getMonth() + 1).padStart(2, '0') + '-' +
615 String(date.getDate()).padStart(2, '0');
616 },
617
618 /**
619 * Format number with commas for thousands
620 */
621 formatNumber: function(num) {
622 return parseFloat(num).toLocaleString('en-US', {
623 minimumFractionDigits: 2,
624 maximumFractionDigits: 2
625 });
626 },
627
628 /**
629 * Capitalize first letter of a string
630 */
631 capitalizeFirst: function(str) {
632 if (!str) return '';
633 return str.charAt(0).toUpperCase() + str.slice(1);
634 },
635
636 /**
637 * Escape values for CSV
638 */
639 escapeCsvValue: function(value) {
640 if (!value) return '';
641 // If the value contains a comma, quote, or newline, wrap in quotes and escape internal quotes
642 if (/[",\n\r]/.test(value)) {
643 return '"' + value.replace(/"/g, '""') + '"';
644 }
645 return value;
646 }
647 };
648
649 // Initialize when document is ready
650 $(document).ready(function() {
651 EasyInvoiceReports.init();
652 });
653
654 })(jQuery);