PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11.1
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / assets / js / admin / charitable-admin-dashboard.js

charitable-admin-dashboard.js in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.11.1, at assets/js/admin/charitable-admin-dashboard.js

1,023 lines 41.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Charitable Admin Dashboard JavaScript
3 *
4 * @package Charitable/Admin/Dashboard
5 * @since 1.8.1
6 * @version 1.8.1
7 */
8
9 (function($) {
10 'use strict';
11
12 /**
13 * Dashboard functionality
14 */
15 var CharitableDashboard = {
16
17 /**
18 * Chart instance for updates
19 */
20 chartInstance: null,
21
22 /**
23 * Initialize the dashboard
24 */
25 init: function() {
26 this.bindEvents();
27 this.initComponents();
28 this.hideInitialLoadingSpinner();
29
30 // Also hide spinners immediately if we're on the dashboard page
31 this.hideSpinnersOnPageLoad();
32
33 // Add timeout fallback to ensure spinner is always hidden after 10 seconds
34 var self = this;
35 setTimeout(function() {
36 // Force hide spinner after 10 seconds as fallback
37 $('#charitable-dashboard-v2').addClass('charitable-dashboard-loaded');
38 $('body.charitable_page_charitable-dashboard').addClass('charitable-dashboard-loaded');
39 }, 10000); // 10 seconds
40 },
41
42 /**
43 * Bind event handlers
44 */
45 bindEvents: function() {
46 // Collapsible section functionality
47 $(document).on('click', '.charitable-dashboard-v2-toggle', this.handleToggleClick);
48
49 // Time period dropdown functionality
50 $(document).on('change', '#time-period-filter', this.handleTimePeriodChange);
51
52 // Enhance campaign section functionality
53 $(document).on('click', '.charitable-dashboard-v2-enhance-grid-button', this.handleEnhanceButtonClick);
54 },
55
56 /**
57 * Initialize dashboard components
58 */
59 initComponents: function() {
60 // Show loading state for chart while we check cookie
61 this.showChartLoadingState();
62
63 // Show "-" for all stats initially
64 this.showLoadingStats();
65
66 // Set initial dropdown value from cookie and initialize chart
67 this.setInitialDropdownValue();
68 },
69
70 /**
71 * Handle toggle click for collapsible sections
72 * Arrow points down when expanded, right when collapsed
73 */
74 handleToggleClick: function(e) {
75 e.preventDefault();
76
77 var $section = $(this).closest('.charitable-dashboard-v2-collapsible');
78 var $content = $section.find('.charitable-dashboard-v2-section-content');
79
80 if ($section.hasClass('charitable-dashboard-v2-collapsed')) {
81 // Expand - slide down
82 $section.removeClass('charitable-dashboard-v2-collapsed');
83
84 // Remove any inline max-height styles
85 $content.css('max-height', '');
86
87 // Let CSS handle the transition back to normal
88 } else {
89 // Collapse - slide up
90 // Get the current height before collapsing
91 var currentHeight = $content[0].scrollHeight;
92
93 // Set max-height to current height for smooth animation
94 $content.css('max-height', currentHeight + 'px');
95
96 // Force reflow
97 $content[0].offsetHeight;
98
99 // Add collapsed class which will animate to max-height: 0
100 $section.addClass('charitable-dashboard-v2-collapsed');
101 }
102 },
103
104 /**
105 * Handle time period dropdown change
106 */
107 handleTimePeriodChange: function(e) {
108 e.preventDefault();
109
110 var timePeriod = $(this).val();
111
112 // Save selection to cookie
113 CharitableDashboard.setCookie('charitable_dashboard_time_period', timePeriod, 30);
114
115 // Update print button date range
116 CharitableDashboard.updatePrintButtonDateRange(timePeriod);
117
118 // Show loading states
119 CharitableDashboard.showLoadingStates();
120
121 // Make AJAX request
122 CharitableDashboard.updateDashboardData(timePeriod);
123 },
124
125 /**
126 * Show loading states for stats and chart
127 */
128 showLoadingStates: function() {
129 // Add loading class to stats row
130 $('.charitable-dashboard-v2-stats-row').addClass('charitable-dashboard-v2-loading');
131
132 // Add loading class to chart container
133 $('.charitable-dashboard-v2-header-bar-chart').addClass('charitable-dashboard-v2-loading');
134 },
135
136 /**
137 * Hide loading states
138 */
139 hideLoadingStates: function() {
140 // Remove loading class from stats row
141 $('.charitable-dashboard-v2-stats-row').removeClass('charitable-dashboard-v2-loading');
142
143 // Add loaded class to show stats text
144 $('.charitable-dashboard-v2-stats-row').addClass('charitable-dashboard-stats-loaded');
145
146 // Remove loading class from chart container
147 $('.charitable-dashboard-v2-header-bar-chart').removeClass('charitable-dashboard-v2-loading');
148 },
149
150 /**
151 * Update dashboard data via AJAX
152 */
153 updateDashboardData: function(timePeriod) {
154 // Guard: reporting data is only localized on the dashboard page.
155 if (typeof charitable_dashboard_reporting === 'undefined') {
156 return;
157 }
158
159 var self = this;
160
161 // Check if we should clear cache based on URL parameter
162 var clearCache = window.location.search.indexOf('charitable_clear_stats_cache=1') !== -1;
163
164 $.ajax({
165 url: charitable_dashboard_reporting.ajax_url,
166 type: 'POST',
167 data: {
168 action: 'charitable_dashboard_data',
169 time_period: timePeriod,
170 nonce: charitable_dashboard_reporting.dashboard_nonce,
171 charitable_clear_stats_cache: clearCache ? '1' : '0'
172 },
173 success: function(response) {
174 if (response.success) {
175 self.updateStatsRow(response.data.stats);
176 self.updateChart(response.data.chart);
177 } else {
178 console.error('Charitable Dashboard: Error updating data:', response.data);
179 self.showError('Failed to update dashboard data: ' + response.data);
180 }
181 },
182 error: function(xhr, status, error) {
183 console.error('Charitable Dashboard: AJAX error:', error);
184 self.showError('Network error: ' + error);
185 },
186 complete: function() {
187 self.hideLoadingStates();
188 self.hideChartLoadingState();
189 }
190 });
191 },
192
193 /**
194 * Update stats row with new data
195 */
196 updateStatsRow: function(stats) {
197 // Helper function to decode HTML entities
198 function decodeHtmlEntities(str) {
199 var textarea = document.createElement('textarea');
200 textarea.innerHTML = str;
201 return textarea.value;
202 }
203
204 // Update total donations
205 var donationsChangeHtml = '';
206 if (stats.donations_change && stats.donations_change !== '') {
207 var changeValue = parseFloat(stats.donations_change.replace('%', ''));
208 var isZero = changeValue === 0;
209 var isPositive = stats.donations_change.indexOf('+') === 0 || (stats.donations_change.indexOf('-') !== 0 && changeValue > 0);
210 var isNegative = stats.donations_change.indexOf('-') === 0 && changeValue < 0;
211 var svgId = 'mask0_1904_1526_' + Date.now() + '_1';
212 // Positive = green, Negative = red, Zero = black
213 var svgColor = isZero ? '#000000' : (isPositive ? '#31944D' : '#DC2626');
214 // Use up arrow for positive, down arrow for negative, up arrow for zero
215 var svgPath = (isPositive || isZero) ?
216 'M1.82849 9.52507L1.07562 8.804L5.05505 4.96689L7.20609 7.02708L10.0024 4.37458H8.60426V3.34448H11.8308V6.43477H10.7553V5.09565L7.20609 8.49497L5.05505 6.43477L1.82849 9.52507Z' :
217 'M1.82849 3.47493L1.07562 4.196L5.05505 8.03311L7.20609 5.97292L10.0024 8.62542H8.60426V9.65552H11.8308V6.56523H10.7553V7.90435L7.20609 4.50503L5.05505 6.56523L1.82849 3.47493Z';
218
219 // Add class based on value: positive, negative, or zero
220 var changeClass = isZero ? 'charitable-dashboard-v2-stat-change-zero' : (isPositive ? 'charitable-dashboard-v2-stat-change-positive' : 'charitable-dashboard-v2-stat-change-negative');
221 donationsChangeHtml = '<span class="charitable-dashboard-v2-stat-change ' + changeClass + '">' +
222 '<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">' +
223 '<mask id="' + svgId + '" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="13" height="13">' +
224 '<rect y="0.25415" width="12.9062" height="12.3612" fill="#D9D9D9"></rect>' +
225 '</mask>' +
226 '<g mask="url(#' + svgId + ')">' +
227 '<path d="' + svgPath + '" fill="' + svgColor + '"></path>' +
228 '</g>' +
229 '</svg>' +
230 stats.donations_change +
231 '</span>';
232 }
233
234 $('.charitable-dashboard-v2-stat-item').eq(0).find('.charitable-dashboard-v2-stat-amount').html(
235 decodeHtmlEntities(stats.total_donations) + donationsChangeHtml
236 );
237
238 // Update average donations
239 var avgChangeHtml = '';
240 if (stats.avg_change && stats.avg_change !== '') {
241 var changeValue = parseFloat(stats.avg_change.replace('%', ''));
242 var isZero = changeValue === 0;
243 var isPositive = stats.avg_change.indexOf('+') === 0 || (stats.avg_change.indexOf('-') !== 0 && changeValue > 0);
244 var isNegative = stats.avg_change.indexOf('-') === 0 && changeValue < 0;
245 var svgId = 'mask0_1904_1526_' + Date.now() + '_2';
246 // Positive = green, Negative = red, Zero = black
247 var svgColor = isZero ? '#000000' : (isPositive ? '#31944D' : '#DC2626');
248 // Use up arrow for positive, down arrow for negative, up arrow for zero
249 var svgPath = (isPositive || isZero) ?
250 'M1.82849 9.52507L1.07562 8.804L5.05505 4.96689L7.20609 7.02708L10.0024 4.37458H8.60426V3.34448H11.8308V6.43477H10.7553V5.09565L7.20609 8.49497L5.05505 6.43477L1.82849 9.52507Z' :
251 'M1.82849 3.47493L1.07562 4.196L5.05505 8.03311L7.20609 5.97292L10.0024 8.62542H8.60426V9.65552H11.8308V6.56523H10.7553V7.90435L7.20609 4.50503L5.05505 6.56523L1.82849 3.47493Z';
252
253 // Add class based on value: positive, negative, or zero
254 var changeClass = isZero ? 'charitable-dashboard-v2-stat-change-zero' : (isPositive ? 'charitable-dashboard-v2-stat-change-positive' : 'charitable-dashboard-v2-stat-change-negative');
255 avgChangeHtml = '<span class="charitable-dashboard-v2-stat-change ' + changeClass + '">' +
256 '<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">' +
257 '<mask id="' + svgId + '" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="13" height="13">' +
258 '<rect y="0.25415" width="12.9062" height="12.3612" fill="#D9D9D9"></rect>' +
259 '</mask>' +
260 '<g mask="url(#' + svgId + ')">' +
261 '<path d="' + svgPath + '" fill="' + svgColor + '"></path>' +
262 '</g>' +
263 '</svg>' +
264 stats.avg_change +
265 '</span>';
266 }
267
268 $('.charitable-dashboard-v2-stat-item').eq(1).find('.charitable-dashboard-v2-stat-amount').html(
269 decodeHtmlEntities(stats.avg_donations) + avgChangeHtml
270 );
271
272 // Update total donors
273 $('.charitable-dashboard-v2-stat-item').eq(2).find('.charitable-dashboard-v2-stat-amount').text(stats.total_donors);
274
275 // Update total refunds
276 $('.charitable-dashboard-v2-stat-item').eq(3).find('.charitable-dashboard-v2-stat-amount').text(decodeHtmlEntities(stats.total_refunds));
277 },
278
279 /**
280 * Update chart with new data
281 */
282 updateChart: function(chartData) {
283 if (this.chartInstance) {
284 // Update the existing chart with new data
285 this.chartInstance.updateOptions({
286 series: [{
287 name: 'Donations',
288 data: chartData.donation_axis
289 }],
290 xaxis: {
291 categories: chartData.date_axis
292 }
293 });
294 } else {
295 // Chart not initialized yet, initialize it with the new data
296 this.initHeadlineChartWithData(chartData);
297 }
298 },
299
300 /**
301 * Show error message
302 */
303 showError: function(message) {
304 // You can implement a notification system here
305 // For now, we'll just log to console
306 console.error('Charitable Dashboard Error:', message);
307
308 // TODO: Future enhancement - show user-friendly error notification
309 // This could be a toast notification or inline error message
310 },
311
312 /**
313 * Show loading state for chart during initial load
314 */
315 showChartLoadingState: function() {
316 $('.charitable-dashboard-v2-header-bar-chart').addClass('charitable-dashboard-v2-loading');
317 },
318
319 /**
320 * Show loading state for stats with "-" placeholders
321 */
322 showLoadingStats: function() {
323 // Update total donations with "-" and no percentage
324 $('.charitable-dashboard-v2-stat-item').eq(0).find('.charitable-dashboard-v2-stat-amount').html('-');
325
326 // Update average donations with "-" and no percentage
327 $('.charitable-dashboard-v2-stat-item').eq(1).find('.charitable-dashboard-v2-stat-amount').html('-');
328
329 // Update total donors with "-"
330 $('.charitable-dashboard-v2-stat-item').eq(2).find('.charitable-dashboard-v2-stat-amount').text('-');
331
332 // Update total refunds with "-"
333 $('.charitable-dashboard-v2-stat-item').eq(3).find('.charitable-dashboard-v2-stat-amount').text('-');
334 },
335
336 /**
337 * Hide loading state for chart
338 */
339 hideChartLoadingState: function() {
340 $('.charitable-dashboard-v2-header-bar-chart').removeClass('charitable-dashboard-v2-loading');
341 },
342
343 /**
344 * Set initial dropdown value from cookie
345 */
346 setInitialDropdownValue: function() {
347 var savedTimePeriod = this.getCookie('charitable_dashboard_time_period');
348 if (savedTimePeriod && $('#time-period-filter option[value="' + savedTimePeriod + '"]').length > 0) {
349 $('#time-period-filter').val(savedTimePeriod);
350
351 // Show loading state for stats row
352 $('.charitable-dashboard-v2-stats-row').addClass('charitable-dashboard-v2-loading');
353
354 // Add a small delay to allow the default 7-day stats to load first
355 var self = this;
356 setTimeout(function() {
357 // Update the dashboard data with the saved time period
358 self.updateDashboardData(savedTimePeriod);
359 }, 500); // 500ms delay to let default stats load first
360 } else {
361 // No cookie found, load default 7-day data
362 var self = this;
363 setTimeout(function() {
364 // Update the dashboard data with default 7-day period
365 self.updateDashboardData('last-7-days');
366 }, 500); // 500ms delay for consistency
367 }
368 },
369
370 /**
371 * Set a cookie with the given name, value, and expiration days
372 */
373 setCookie: function(name, value, days) {
374 var expires = '';
375 if (days) {
376 var date = new Date();
377 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
378 expires = '; expires=' + date.toUTCString();
379 }
380
381 // Make cookie unique to user by including user ID if available
382 var userId = typeof charitable_dashboard_reporting !== 'undefined' && charitable_dashboard_reporting.user_id ?
383 charitable_dashboard_reporting.user_id : '0';
384
385 document.cookie = name + '_user_' + userId + '=' + value + expires + '; path=/';
386 },
387
388 /**
389 * Get a cookie value by name
390 */
391 getCookie: function(name) {
392 // Make cookie unique to user by including user ID if available
393 var userId = typeof charitable_dashboard_reporting !== 'undefined' && charitable_dashboard_reporting.user_id ?
394 charitable_dashboard_reporting.user_id : '0';
395
396 var nameEQ = name + '_user_' + userId + '=';
397 var ca = document.cookie.split(';');
398
399 for (var i = 0; i < ca.length; i++) {
400 var c = ca[i];
401 while (c.charAt(0) === ' ') {
402 c = c.substring(1, c.length);
403 }
404 if (c.indexOf(nameEQ) === 0) {
405 return c.substring(nameEQ.length, c.length);
406 }
407 }
408 return null;
409 },
410
411 /**
412 * Initialize the headline chart with custom data
413 * Used when loading from cookie
414 */
415 initHeadlineChartWithData: function(chartData) {
416 this.createChart(chartData);
417 },
418
419 /**
420 * Initialize the headline chart for the dashboard
421 * This duplicates the functionality from the main dashboard
422 *
423 * @since 1.8.1
424 */
425 initHeadlineChart: function() {
426
427 // Check if the chart container exists
428 if ($('#charitable-dashboard-v2-headline-graph').length === 0) {
429 console.warn('Charitable Dashboard: Chart container not found');
430 return;
431 }
432
433 // Check if the localized data is available
434 if (typeof charitable_dashboard_reporting === 'undefined') {
435 console.warn('Charitable Dashboard: Chart data not available');
436 return;
437 }
438
439
440 // Use the default chart data from the localized object
441 var chartData = {
442 donation_axis: charitable_dashboard_reporting.headline_chart_options.donation_axis,
443 date_axis: charitable_dashboard_reporting.headline_chart_options.date_axis
444 };
445
446 this.createChart(chartData);
447 },
448
449 /**
450 * Create the chart with the provided data
451 */
452 createChart: function(chartData) {
453 // Check if the chart container exists
454 if ($('#charitable-dashboard-v2-headline-graph').length === 0) {
455 console.warn('Charitable Dashboard: Chart container not found');
456 return;
457 }
458
459 // Check if ApexCharts is available
460 if (typeof ApexCharts === 'undefined') {
461 console.warn('Charitable Dashboard: ApexCharts library not loaded');
462 return;
463 }
464
465 var enableToolTips = false;
466
467 // Check if tooltips are enabled in the localized data
468 if (typeof charitable_dashboard_reporting !== 'undefined' &&
469 typeof charitable_dashboard_reporting.headline_chart_options !== 'undefined' &&
470 charitable_dashboard_reporting.headline_chart_options.enable_tooltips !== false) {
471 enableToolTips = true;
472 }
473
474 // Chart configuration - matches the main dashboard exactly
475 var headlineChartOptions = {
476 chart: {
477 type: 'area',
478 width: '100%',
479 height: 300,
480 foreColor: "#757781",
481 toolbar: {
482 autoSelected: "pan",
483 show: false
484 },
485 events: {
486 // TODO: Add animation end event for future growth tools notice functionality
487 // animationEnd: function ( chartContext, options ) {
488 // // Future: Show growth tools notice if needed
489 // }
490 }
491 },
492 series: [
493 {
494 name: 'Donations',
495 data: chartData.donation_axis
496 }
497 ],
498 colors: ["#5AA15226"],
499 grid: {
500 borderColor: "#C9D4CA",
501 clipMarkers: false,
502 yaxis: {
503 lines: {
504 show: true
505 }
506 }
507 },
508 stroke: {
509 width: 3,
510 colors: ["#5AA152"]
511 },
512 fill: {
513 type: "solid"
514 },
515 tooltip: {
516 enabled: enableToolTips,
517 theme: "dark"
518 },
519 dataLabels: {
520 enabled: false
521 },
522 markers: {
523 size: 5,
524 colors: ["#FFFFFF"],
525 strokeColor: "#5AA152",
526 strokeWidth: 4
527 },
528 xaxis: {
529 categories: chartData.date_axis
530 },
531 yaxis: {
532 labels: {
533 formatter: function(value) {
534 // Format currency values using the configured decimal count.
535 // Currency symbol is already decoded in PHP, so use it directly.
536 var decimalCount = typeof charitable_dashboard_reporting.decimal_count !== 'undefined'
537 ? charitable_dashboard_reporting.decimal_count
538 : 2;
539 return charitable_dashboard_reporting.currency_symbol + value.toFixed(decimalCount);
540 }
541 },
542 min: 0
543 }
544 };
545
546 // Create and render the chart
547 try {
548 var betaHeadlineChart = new ApexCharts(
549 document.querySelector("#charitable-dashboard-v2-headline-graph"),
550 headlineChartOptions
551 );
552
553 betaHeadlineChart.render();
554
555 // Store reference for future updates
556 CharitableDashboard.chartInstance = betaHeadlineChart;
557
558 } catch (error) {
559 console.error('Charitable Dashboard: Error initializing chart:', error);
560 }
561 },
562
563 /**
564 * Example method for initializing widgets
565 */
566 initWidgets: function() {
567 // Add your widget initialization code here
568 },
569
570 /**
571 * Handle enhance campaign button clicks
572 */
573 handleEnhanceButtonClick: function(e) {
574 var $button = $(this);
575 var action = $button.data('action');
576
577 // Allow normal link navigation for elements without a data-action (e.g. Learn More)
578 // or upgrade links that are already <a> tags with href.
579 if (!action || action === 'upgrade') {
580 return;
581 }
582
583 e.preventDefault();
584 var slug = $button.data('slug');
585 var basename = $button.data('basename');
586 var type = $button.data('type');
587 var setupUrl = $button.data('setup-url');
588
589
590 // Store original state for error handling
591 var originalState = {
592 text: $button.text(),
593 class: $button.attr('class')
594 };
595
596 // Handle different actions
597 switch (action) {
598 case 'install':
599 CharitableDashboard.handlePluginInstall($button, slug, type, originalState);
600 break;
601 case 'activate':
602 CharitableDashboard.handlePluginActivate($button, basename || slug, type, originalState);
603 break;
604 case 'install_addon':
605 CharitableDashboard.handleAddonInstall($button, slug, type, originalState);
606 break;
607 case 'activate_addon':
608 CharitableDashboard.handleAddonActivate($button, slug, type, originalState);
609 break;
610 case 'upgrade':
611 var upgradeUrl = $button.data('upgrade-url');
612 if (upgradeUrl) {
613 window.open(upgradeUrl, '_blank');
614 }
615 break;
616 case 'setup':
617 if (setupUrl) {
618 window.location.href = setupUrl;
619 }
620 break;
621 }
622 },
623
624 /**
625 * Handle third-party plugin installation
626 */
627 handlePluginInstall: function($button, slug, type, originalState) {
628 // Show loading state
629 $button.text('Installing...').prop('disabled', true);
630
631 $.ajax({
632 url: ajaxurl,
633 type: 'POST',
634 data: {
635 action: 'charitable_install_plugin',
636 nonce: charitable_admin.nonce,
637 slug: slug
638 },
639 success: function(response) {
640 if (response.success) {
641 // Update button to next state
642 $button.text('Activate').addClass('charitable-dashboard-v2-activate-button').prop('disabled', false);
643 $button.attr('data-action', 'activate').data('action', 'activate');
644 } else {
645 var errorMessage = 'Installation failed';
646 if (response.data && response.data.message) {
647 errorMessage = response.data.message;
648 } else if (response.message) {
649 errorMessage = response.message;
650 }
651 CharitableDashboard.handleError($button, originalState, errorMessage);
652 }
653 },
654 error: function() {
655 CharitableDashboard.handleError($button, originalState, 'Network error. Please try again.');
656 }
657 });
658 },
659
660 /**
661 * Handle third-party plugin activation
662 */
663 handlePluginActivate: function($button, basename, type, originalState) {
664 // Show loading state
665 $button.text('Activating...').prop('disabled', true);
666
667 $.ajax({
668 url: ajaxurl,
669 type: 'POST',
670 data: {
671 action: 'charitable_activate_plugin',
672 nonce: charitable_admin.nonce,
673 basename: basename
674 },
675 success: function(response) {
676 if (response.success) {
677 // Update button to setup state
678 $button.text('Setup').removeClass('charitable-dashboard-v2-activate-button').addClass('charitable-dashboard-v2-setup-button').prop('disabled', false);
679 $button.attr('data-action', 'setup').data('action', 'setup');
680
681 // Set setup URL if provided in response
682 if (response.data && response.data.setup) {
683 $button.attr('data-setup-url', response.data.setup).data('setup-url', response.data.setup);
684 } else if (response.data && response.data.settings) {
685 // Fallback to settings URL if no setup URL
686 $button.attr('data-setup-url', response.data.settings).data('setup-url', response.data.settings);
687 }
688 } else {
689 var errorMessage = 'Activation failed';
690 if (response.data && response.data.message) {
691 errorMessage = response.data.message;
692 } else if (response.message) {
693 errorMessage = response.message;
694 }
695 CharitableDashboard.handleError($button, originalState, errorMessage);
696 }
697 },
698 error: function() {
699 CharitableDashboard.handleError($button, originalState, 'Network error. Please try again.');
700 }
701 });
702 },
703
704 /**
705 * Handle Charitable addon installation
706 */
707 handleAddonInstall: function($button, slug, type, originalState) {
708 // Show loading state
709 $button.text('Installing...').prop('disabled', true);
710
711 $.ajax({
712 url: ajaxurl,
713 type: 'POST',
714 data: {
715 action: 'charitable_install_charitable_addon',
716 nonce: charitable_admin.nonce,
717 slug: slug
718 },
719 success: function(response) {
720 if (response.success) {
721 if (response.data.redirect_url) {
722 window.location.href = response.data.redirect_url;
723 } else {
724 // Update button to next state
725 $button.text('Activate').addClass('charitable-dashboard-v2-activate-button').prop('disabled', false);
726 $button.attr('data-action', 'activate_addon').data('action', 'activate_addon');
727 }
728 } else {
729 var errorMessage = 'Installation failed';
730 if (response.data && response.data.message) {
731 errorMessage = response.data.message;
732 } else if (response.message) {
733 errorMessage = response.message;
734 }
735 CharitableDashboard.handleError($button, originalState, errorMessage);
736 }
737 },
738 error: function() {
739 CharitableDashboard.handleError($button, originalState, 'Network error. Please try again.');
740 }
741 });
742 },
743
744 /**
745 * Handle Charitable addon activation
746 */
747 handleAddonActivate: function($button, slug, type, originalState) {
748 // Show loading state
749 $button.text('Activating...').prop('disabled', true);
750
751 $.ajax({
752 url: ajaxurl,
753 type: 'POST',
754 data: {
755 action: 'charitable_activate_charitable_addon',
756 nonce: charitable_admin.nonce,
757 slug: slug
758 },
759 success: function(response) {
760 if (response.success) {
761 if (response.data.redirect_url) {
762 window.location.href = response.data.redirect_url;
763 } else {
764 // Update button based on response data
765 var buttonText = response.data.button_text || 'Installed';
766 var buttonClass = response.data.button_class || 'charitable-dashboard-v2-installed-button';
767 var buttonAction = response.data.button_action || 'installed';
768 var isDisabled = response.data.button_disabled || false;
769
770 $button.text(buttonText)
771 .removeClass('charitable-dashboard-v2-activate-button')
772 .addClass(buttonClass)
773 .prop('disabled', isDisabled)
774 .attr('data-action', buttonAction)
775 .data('action', buttonAction);
776 }
777 } else {
778 var errorMessage = 'Activation failed';
779 if (response.data && response.data.message) {
780 errorMessage = response.data.message;
781 } else if (response.message) {
782 errorMessage = response.message;
783 }
784 CharitableDashboard.handleError($button, originalState, errorMessage);
785 }
786 },
787 error: function() {
788 CharitableDashboard.handleError($button, originalState, 'Network error. Please try again.');
789 }
790 });
791 },
792
793 /**
794 * Handle errors with admin styling
795 */
796 handleError: function($button, originalState, errorMessage) {
797 // Show error message
798 var $errorElement = $('<div class="charitable-dashboard-v2-error-message" style="color: #d63638; font-size: 12px; margin-top: 5px;">' + errorMessage + '</div>');
799
800 // Insert error message after button
801 $button.after($errorElement);
802
803 // Revert button to original state
804 $button.text(originalState.text).attr('class', originalState.class).prop('disabled', false);
805
806 // Remove error message after 5 seconds
807 setTimeout(function() {
808 $errorElement.fadeOut(function() {
809 $errorElement.remove();
810 });
811 }, 5000);
812 },
813
814 /**
815 * Hide the initial loading spinner
816 */
817 hideInitialLoadingSpinner: function() {
818 var self = this;
819
820 // Wait for DOM to be fully loaded and then hide spinner
821 $(document).ready(function() {
822 // Add a small delay to ensure all content is rendered
823 setTimeout(function() {
824 self.hideSpinners();
825 }, 100);
826
827 // Also hide spinners when dashboard content is detected
828 self.waitForDashboardContent();
829 });
830 },
831
832 /**
833 * Wait for dashboard content to load and hide spinners
834 */
835 waitForDashboardContent: function() {
836 var self = this;
837 var attempts = 0;
838 var maxAttempts = 50; // 5 seconds max wait time
839
840 var checkForContent = function() {
841 attempts++;
842
843 // Check if dashboard content is present
844 var hasContent = $('#charitable-dashboard-v2 .charitable-dashboard-v2-content').length > 0 ||
845 $('#charitable-dashboard-report-container').length > 0 ||
846 $('.charitable-dashboard-report').length > 0;
847
848 if (hasContent || attempts >= maxAttempts) {
849 self.hideSpinners();
850 } else {
851 setTimeout(checkForContent, 100);
852 }
853 };
854
855 checkForContent();
856 },
857
858 /**
859 * Hide all dashboard spinners
860 * Note: There's also a 10-second timeout fallback in init() to ensure spinner is always hidden
861 */
862 hideSpinners: function() {
863 // Hide the dashboard v2 spinner
864 $('#charitable-dashboard-v2').addClass('charitable-dashboard-loaded');
865
866 // Hide the global dashboard spinner (targets body class)
867 $('body.charitable_page_charitable-dashboard').addClass('charitable-dashboard-loaded');
868 },
869
870 /**
871 * Hide spinners on page load - immediate fallback
872 */
873 hideSpinnersOnPageLoad: function() {
874 var self = this;
875
876 // Hide spinners after a short delay to ensure they're visible briefly
877 setTimeout(function() {
878 self.hideSpinners();
879 }, 500);
880 },
881
882 /**
883 * Update print button date range based on time period
884 */
885 updatePrintButtonDateRange: function(timePeriod) {
886 var dateRange = this.getDateRangeForPeriod(timePeriod);
887
888 // Update the print form hidden fields
889 $('#charitable-dashboard-v2-print input[name="start_date"]').val(dateRange.start_date);
890 $('#charitable-dashboard-v2-print input[name="end_date"]').val(dateRange.end_date);
891 $('#charitable-dashboard-v2-print input[name="days"]').val(timePeriod);
892 },
893
894 /**
895 * Get date range for a given time period
896 */
897 getDateRangeForPeriod: function(timePeriod) {
898 var today = new Date();
899 var startDate, endDate;
900
901 switch(timePeriod) {
902 case 'last-7-days':
903 startDate = new Date(today.getTime() - (7 * 24 * 60 * 60 * 1000));
904 break;
905 case 'last-14-days':
906 startDate = new Date(today.getTime() - (14 * 24 * 60 * 60 * 1000));
907 break;
908 case 'last-30-days':
909 startDate = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
910 break;
911 case 'last-3-months':
912 startDate = new Date(today.getTime() - (90 * 24 * 60 * 60 * 1000));
913 break;
914 case 'last-6-months':
915 startDate = new Date(today.getTime() - (180 * 24 * 60 * 60 * 1000));
916 break;
917 case 'last-year':
918 startDate = new Date(today.getTime() - (365 * 24 * 60 * 60 * 1000));
919 break;
920 default:
921 startDate = new Date(today.getTime() - (7 * 24 * 60 * 60 * 1000));
922 }
923
924 endDate = today;
925
926 return {
927 start_date: this.formatDateForPrint(startDate),
928 end_date: this.formatDateForPrint(endDate)
929 };
930 },
931
932 /**
933 * Format date for print functionality (YYYY-MM-DD format)
934 */
935 formatDateForPrint: function(date) {
936 var year = date.getFullYear();
937 var month = String(date.getMonth() + 1).padStart(2, '0');
938 var day = String(date.getDate()).padStart(2, '0');
939 return year + '-' + month + '-' + day;
940 }
941
942 };
943
944 /**
945 * Initialize when document is ready
946 */
947 $(document).ready(function() {
948 CharitableDashboard.init();
949 initProgressCircles();
950 initTabSwitching();
951 });
952
953 /**
954 * Initialize progress circles
955 */
956 function initProgressCircles() {
957 const progressCircles = document.querySelectorAll('.progress-circle');
958
959 progressCircles.forEach(circle => {
960 const progress = parseInt(circle.getAttribute('data-progress')) || 0;
961 const svg = circle.querySelector('svg');
962 const progressCircle = svg.querySelector('circle:last-child');
963
964 if (progressCircle) {
965 // Calculate circle properties
966 const radius = 18; // r="18" from the SVG
967 const circumference = 2 * Math.PI * radius;
968
969 // Set the stroke-dasharray to the full circumference
970 progressCircle.style.strokeDasharray = circumference;
971
972 // Calculate stroke-dashoffset to show progress
973 // Start from top (0 degrees) and go clockwise
974 // For 0% progress, offset should be circumference (full circle hidden)
975 // For 100% progress, offset should be 0 (full circle visible)
976 const offset = circumference - (progress / 100) * circumference;
977 progressCircle.style.strokeDashoffset = offset;
978
979 // Rotate the progress circle by -90 degrees to start from the top
980 progressCircle.style.transform = 'rotate(-90deg)';
981 // Set the transform origin to the center of the circle
982 progressCircle.style.transformOrigin = '20px 20px';
983 }
984 });
985 }
986
987 /**
988 * Initialize tab switching functionality
989 */
990 function initTabSwitching() {
991 const tabNavItems = document.querySelectorAll('.charitable-dashboard-v2-tab-nav-item');
992 const tabContents = document.querySelectorAll('.charitable-dashboard-v2-tab-content');
993
994 tabNavItems.forEach(tab => {
995 tab.addEventListener('click', function(e) {
996 e.preventDefault();
997
998 const targetTab = this.getAttribute('data-tab');
999
1000 // Remove active class from all tabs and contents
1001 tabNavItems.forEach(item => item.classList.remove('charitable-dashboard-v2-tab-nav-active'));
1002 tabContents.forEach(content => content.classList.remove('charitable-dashboard-v2-tab-content-active'));
1003
1004 // Add active class to clicked tab
1005 this.classList.add('charitable-dashboard-v2-tab-nav-active');
1006
1007 // Show corresponding content - specifically target tab content divs
1008 const targetContent = document.querySelector(`.charitable-dashboard-v2-tab-content[data-tab="${targetTab}"]`);
1009 if (targetContent) {
1010 targetContent.classList.add('charitable-dashboard-v2-tab-content-active');
1011 }
1012
1013 // Reinitialize progress circles if switching to campaigns tab
1014 if (targetTab === 'top-campaigns') {
1015 setTimeout(() => {
1016 initProgressCircles();
1017 }, 100);
1018 }
1019 });
1020 });
1021 }
1022
1023 })(jQuery);