| 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); |