PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
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 1.7.0.9 All 211 releases
charitable / includes / admin / reports / class-charitable-reports.php

class-charitable-reports.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at includes/admin/reports/class-charitable-reports.php

4,339 lines 153.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Reports UI.
4 *
5 * @package Charitable/Classes/Charitable_Reports
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.8.1
10 * @version 1.8.1
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Reports' ) ) :
19
20 /**
21 * Charitable_Reports
22 *
23 * @final
24 * @since 1.8.1
25 */
26 class Charitable_Reports {
27
28 /**
29 * The single instance of this class.
30 *
31 * @var Charitable_Reports|null
32 */
33 private static $instance = null;
34
35 /**
36 * The date filter values.
37 *
38 * @since 1.8.1
39 *
40 * @var array
41 */
42 public $date_filter_values = array();
43
44 /**
45 * The report type.
46 *
47 * @since 1.8.1
48 *
49 * @var string
50 */
51 public $report_type = false;
52
53 /**
54 * The start date.
55 *
56 * @since 1.8.1
57 *
58 * @var string
59 */
60 public $start_date = false;
61
62 /**
63 * The end date.
64 *
65 * @since 1.8.1
66 *
67 * @var string
68 */
69 public $end_date = false;
70
71 /**
72 * The key filter for the report.
73 *
74 * @since 1.8.1
75 *
76 * @var string
77 */
78 public $filter = false;
79
80 /**
81 * The (post) status filter for the report, usually reflecting post_status.
82 *
83 * @since 1.8.1
84 *
85 * @var string
86 */
87 public $status = false;
88
89 /**
90 * The campaign ID filter for the report.
91 *
92 * @since 1.8.1
93 *
94 * @var string
95 */
96 public $campaign_id = false;
97
98 /**
99 * The Charitable custom tax category ID filter for the report.
100 *
101 * @since 1.8.1
102 *
103 * @var string
104 */
105 public $category_id = false;
106
107 /**
108 * The donation data axis for the highlight report.
109 *
110 * @since 1.8.1
111 *
112 * @var array
113 */
114 public $donation_axis = array();
115
116 /**
117 * The date axis for the highlight report.
118 *
119 * @since 1.8.1
120 *
121 * @var array
122 */
123 public $date_axis = array();
124
125 /**
126 * The donation data.
127 *
128 * @since 1.8.1
129 *
130 * @var array
131 */
132 public $donations = array();
133
134 /**
135 * The donor data.
136 *
137 * @since 1.8.1
138 *
139 * @var array
140 */
141 public $donors = array();
142
143 /**
144 * The donor total for the query.
145 *
146 * @since 1.8.1
147 *
148 * @var array
149 */
150 public $donors_total = 0;
151
152 /**
153 * The payment percentages data for the payment report/graph.
154 *
155 * @since 1.8.1
156 *
157 * @var array
158 */
159 public $payment_percentages = array( '100' );
160
161 /**
162 * The payment percentages labels for the payment report/graph.
163 *
164 * @since 1.8.1
165 *
166 * @var array
167 */
168 public $payment_labels = array( 'No donations found' );
169
170 /**
171 * The payment gateway keys for the payment report/graph (used for color mapping).
172 *
173 * @since 1.8.1
174 *
175 * @var array
176 */
177 public $payment_keys = array();
178
179 /**
180 * The activities data.
181 *
182 * @since 1.8.1
183 *
184 * @var array
185 */
186 public $activities = array();
187
188 /**
189 * The dot ... threshold for the pagination.
190 *
191 * @since 1.8.1
192 *
193 * @var array
194 */
195 public $pagination_dot_threshold = 5;
196
197 /**
198 * Create object instance.
199 *
200 * @since 1.8.1
201 */
202 public function __construct() {
203 }
204
205 /**
206 * Run things upon init.
207 *
208 * @since 1.8.1
209 *
210 * @param string $report_type The report type.
211 * @param string $start_date The start date.
212 * @param string $end_date The end date.
213 * @param string $status The status.
214 * @param int $campaign_id The campaign ID.
215 * @param int $category_id The category ID.
216 *
217 * @return void
218 */
219 public function init( $report_type = 'overview', $start_date = false, $end_date = false, $status = false, $campaign_id = false, $category_id = false ) {
220
221 do_action( 'charitable_admin_reports_init_start', $this );
222
223 // Get the date filter values.
224 $this->date_filter_values = apply_filters(
225 'charitable_dashboard_date_filter_values',
226 array(
227 '7' => __( 'Last 7 Days', 'charitable' ),
228 '14' => __( 'Last 14 Days', 'charitable' ),
229 '30' => __( 'Last 30 Days', 'charitable' ),
230 '0' => __( 'Custom Date Range', 'charitable' ),
231 )
232 );
233
234 $this->report_type = $report_type;
235 $this->start_date = $start_date;
236 $this->end_date = $end_date;
237 $this->status = $status;
238 $this->campaign_id = $campaign_id;
239 $this->category_id = $category_id;
240
241 // Customize how we grab data from the database based on the report type.
242 if ( $this->is_report_type_donor( $report_type ) ) {
243 $this->get_donors_data( $report_type, $start_date, $end_date, $campaign_id, $category_id );
244 } else {
245 $this->get_data( $report_type, $start_date, $end_date, $status, $campaign_id, $category_id );
246 }
247
248 $this->maybe_load_scripts();
249 $this->maybe_add_reports_cta();
250
251 do_action( 'charitable_admin_reports_init_end', $this );
252 }
253
254 /**
255 * Run things upon init.
256 *
257 * @since 1.8.1
258 *
259 * @param string $report_type The report type.
260 * @param array $args The report args.
261 *
262 * @return void
263 */
264 public function init_with_array( $report_type = 'overview', $args = array() ) {
265
266 do_action( 'charitable_admin_reports_init_start', $this );
267
268 // Get the date filter values.
269 $this->date_filter_values = apply_filters(
270 'charitable_dashboard_date_filter_values',
271 array(
272 '7' => __( 'Last 7 Days', 'charitable' ),
273 '14' => __( 'Last 14 Days', 'charitable' ),
274 '30' => __( 'Last 30 Days', 'charitable' ),
275 '0' => __( 'Custom Date Range', 'charitable' ),
276 )
277 );
278
279 $defaults = array(
280 'report_type' => 'overview',
281 'start_date' => false,
282 'end_date' => false,
283 'status' => false,
284 'campaign_id' => false,
285 'category_id' => false,
286 'limit' => false,
287 'offset' => false,
288 'ppage' => false,
289 );
290
291 $args = wp_parse_args( $args, $defaults );
292
293 $this->report_type = $args['report_type'];
294 $this->start_date = $args['start_date'];
295 $this->end_date = $args['end_date'];
296 $this->status = $args['status'];
297 $this->campaign_id = $args['campaign_id'];
298 $this->category_id = $args['category_id'];
299
300 // Customize how we grab data from the database based on the report type.
301 if ( $this->is_report_type_donor( $report_type ) ) {
302 // override the report type in $args with what was passed in.
303 $args['report_type'] = $report_type;
304
305 $this->get_donors_data_array( $report_type, $args );
306 } else {
307 $this->get_data( $report_type, $args );
308 }
309
310 $this->maybe_load_scripts();
311 $this->maybe_add_reports_cta();
312
313 do_action( 'charitable_admin_reports_init_end', $this );
314 }
315
316 /**
317 * Generate HTML for the Top Campaigns list.
318 *
319 * @since 1.8.1
320 * @version 1.8.3 Make status translatable.
321 *
322 * @param object $top_campaigns The top campaigns data.
323 * @param bool $include_icons Whether to include icons.
324 *
325 * @return string
326 */
327 public function generate_top_campaigns( $top_campaigns = false, $include_icons = true ) {
328
329 if ( empty( $top_campaigns ) || empty( $top_campaigns->posts ) ) {
330 return;
331 }
332
333 ob_start();
334
335 foreach ( $top_campaigns->posts as $campaign ) :
336
337 $_campaign = $campaign->ID ? charitable_get_campaign( $campaign->ID ) : false;
338 $status = $_campaign ? $_campaign->get_status() : false;
339 switch ( $status ) {
340 case 'active':
341 $status_string = esc_html__( 'Active', 'charitable' );
342 break;
343 case 'draft':
344 $status_string = esc_html__( 'Draft', 'charitable' );
345 break;
346 case 'completed':
347 $status_string = esc_html__( 'Completed', 'charitable' );
348 break;
349 case 'failed':
350 $status_string = esc_html__( 'Failed', 'charitable' );
351 break;
352 default:
353 $status_string = ucwords( $status );
354 break;
355 }
356 $donated_amount = $_campaign->get_donated_amount();
357 $donor_count = $_campaign->get_donor_count();
358 $donation_goal = $_campaign->get_goal();
359 $time_ended = $_campaign->get_time_since_ended();
360 $end_date = $_campaign->get_end_date();
361 $percent_donated = $_campaign->get_percent_donated_raw();
362 if ( $percent_donated > 100 ) {
363 $percent_donated = 100;
364 }
365
366 ?>
367 <li class="campaign-completed">
368 <div class="main">
369 <div class="info">
370 <p class="campaign-title"><a href="<?php echo esc_url( charitable_get_admin_campaign_edit_url( $campaign->ID ) ); ?>" target="_blank"><?php echo esc_html( $campaign->post_title ); ?></a></p>
371 <div class="info-summary">
372 <p class="total-donations"><?php echo esc_html__( 'Total Donations', 'charitable' ); ?>: <strong><?php echo charitable_format_money( $donated_amount ); // phpcs:ignore ?></strong></p>
373 <p class="cr"><?php echo esc_html__( 'Total Donors', 'charitable' ); ?>: <strong><?php echo intval( $donor_count ); ?></strong></p>
374 <?php if ( $donation_goal ) : ?>
375 <p class="cr"><?php echo esc_html__( 'Goal', 'charitable' ); ?>: <strong><?php echo charitable_format_money( $donation_goal ); // phpcs:ignore ?></strong></p>
376 <?php endif; ?>
377 <?php if ( $end_date ) : ?>
378 <p class="cr"><?php echo esc_html__( 'End Date', 'charitable' ); ?>: <strong><?php echo esc_html( $end_date ); ?></strong></p>
379 <?php endif; ?>
380 </div>
381 </div>
382 <div class="status">
383 <p><span class="badge <?php echo esc_attr( $status ); ?>"><?php echo esc_html( $status_string ); ?></span></p>
384 </div>
385 </div>
386 <?php
387 if ( ! $include_icons ) :
388 ?>
389 <br class="clear" /><?php endif; ?>
390 <div class="progress-bar">
391 <?php
392 if ( $donated_amount > 0 && $donation_goal && $percent_donated > 0 ) :
393 ?>
394 <div class="progress" style="width: <?php echo intval( $percent_donated ); ?>%"></div>
395 <?php
396 endif;
397 ?>
398 </div>
399 <?php
400 if ( ! $include_icons ) :
401 ?>
402 <br class="clear" /><?php endif; ?>
403 </li>
404 <?php endforeach; ?>
405 <?php
406
407 return ob_get_clean();
408 }
409
410 /**
411 * Generate HTML for the Top Donor List
412 *
413 * @since 1.8.1
414 *
415 * @param array $donors The donor data.
416 * @param bool $include_icons Whether to include icons.
417 *
418 * @return string
419 */
420 public function generate_top_donors( $donors = array(), $include_icons = true ) {
421
422 ob_start();
423
424 if ( empty( $donors ) ) {
425 ?>
426
427 <div class="no-items the-list">
428 <p><strong><?php echo esc_html__( 'There are no donors within the date range.', 'charitable' ); ?></strong></p>
429 </div>
430
431 <?php } else { ?>
432
433 <div class="the-list">
434 <ul id="charitable-top-donors-list">
435
436 <?php
437
438 $unique_donors = array();
439
440 foreach ( $donors as $top_donor ) :
441
442 if ( in_array( $top_donor->donor_id, $unique_donors, true ) ) {
443 continue;
444 }
445
446 // if this is a donation related to a recurring donation, then this parent will have a type of 'recurring'.
447 $donation_parent = ! empty( $top_donor->donation_parent_id ) ? charitable_get_donation( $top_donor->donation_parent_id )->get( 'donation_type' ) : false;
448
449 $donor_name = ! empty( $top_donor->donor_name ) ? esc_html( $top_donor->donor_name ) : 'Unknown';
450 $donor_avatar = ! empty( $top_donor->donor_avatar_url ) ? esc_url( $top_donor->donor_avatar_url ) : false;
451 if ( false === $donor_avatar && function_exists( 'get_avatar_url' ) && ! empty( $top_donor->donor_email ) ) {
452 $donor_avatar = get_avatar_url(
453 $top_donor->donor_email,
454 array(
455 'size' => 50,
456 )
457 );
458 }
459 $badge_css = strtolower( $donation_parent ) === 'recurring' ? 'recurring' : 'one-time';
460 $badge_label = strtolower( $donation_parent ) === 'recurring' ? esc_html__( 'Recurring', 'charitable' ) : esc_html__( 'One-Time', 'charitable' );
461
462 $display_amount = ! empty( $top_donor->total_donation_amount ) ? charitable_format_money( $top_donor->total_donation_amount, 2, true ) : charitable_format_money( esc_html( $top_donor->amount ), 2, true );
463 $display_count = ! empty( $top_donor->total_donation_count ) ? intval( $top_donor->total_donation_count ) : 1;
464
465 // based on display count, override the badge css/label.
466 if ( $display_count > 1 ) {
467 $badge_css = 'multiple';
468 $badge_label = esc_html__( 'Multiple', 'charitable' );
469 }
470
471 // ensure we don't display the same donor twice.
472 $unique_donors[] = $top_donor->donor_id;
473
474 ?>
475 <li>
476 <?php if ( $include_icons ) : ?>
477 <div class="avatar">
478 <img src="<?php echo esc_url( $donor_avatar ); ?>" alt="<?php echo esc_html( $donor_name ); ?>">
479 </div>
480 <?php endif; ?>
481 <div class="info">
482 <p class="name">
483 <?php echo esc_html( $donor_name ); ?>
484 </p>
485 <?php if ( ! empty( $top_donor->donor_email ) ) : ?>
486 <p class="email"><?php echo esc_html( $top_donor->donor_email ); ?></p>
487 <?php endif; ?>
488 </div>
489 <div class="donor-donation-info">
490 <p><span class="amount"><?php echo $display_amount; // phpcs:ignore ?></span></p>
491 <p><span class="badge <?php echo esc_attr( $badge_css ); ?>"><?php echo esc_html( $badge_label ); ?></span></p>
492 </div>
493 <?php
494 if ( ! $include_icons ) :
495 ?>
496 <br class="clear" /><?php endif; ?>
497 </li>
498 <?php endforeach; ?>
499 </ul>
500 </div>
501
502 <?php
503 }
504
505 return ob_get_clean();
506 }
507
508 /**
509 * Generate HTML for the Recent Donation List (appears on dashboard).
510 *
511 * @since 1.8.1
512 *
513 * @param array $donations The donation data.
514 * @param bool $limit Whether to limit the number of donations.
515 * @param bool $include_icons Whether to include icons.
516 *
517 * @return string
518 */
519 public function generate_recent_donations_list( $donations = array(), $limit = false, $include_icons = true ) { // phpcs:ignore
520
521 if ( empty( $donations ) ) {
522 return;
523 }
524
525 $limit_counter = 0;
526
527 ob_start();
528
529 $statuses = charitable_get_valid_donation_statuses();
530
531 // reverse the donations array.
532 $donations = array_reverse( $donations );
533
534 foreach ( $donations as $donation ) :
535
536 if ( $limit && $limit_counter >= $limit ) {
537 break;
538 }
539
540 $donation_id = intval( $donation->donation_id );
541 $donor_name = esc_html( $donation->first_name ) . ' ' . esc_html( $donation->last_name );
542 $payment_method = esc_html( $donation->payment_gateway );
543 $donation_status = esc_html( $donation->donation_status );
544
545 $donation_status_label = array_key_exists( $donation_status, $statuses ) ? $statuses[ $donation_status ] : $donation_status;
546
547 ?>
548 <li>
549 <div class="main">
550 <div class="info">
551 <p class="donor-id-name">#<?php echo esc_html( $donation_id ); ?> <?php echo esc_html__( 'by', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?></p>
552 <div class="info-summary">
553 <p class="amount-donated"><?php echo esc_html__( 'Amount Donated', 'charitable' ); ?>: <strong><?php echo charitable_format_money( $donation->amount ); // phpcs:ignore ?></strong></p>
554 <p class="amount-donated"><?php echo esc_html__( 'Method', 'charitable' ); ?>: <strong><?php echo esc_html( ucwords( $payment_method ) ); ?></strong></p>
555 </div>
556 </div>
557 <div class="status">
558 <p><span class="badge <?php echo esc_attr( strtolower( $donation_status ) ); ?>"><?php echo esc_html( $donation_status_label ); ?></span></p>
559 </div>
560 </div>
561 </li>
562 <?php
563
564 ++$limit_counter;
565
566 endforeach;
567
568 return ob_get_clean();
569 }
570
571 /**
572 * Return the HTML of the activity filter select dropdown.
573 *
574 * @since 1.8.1
575 *
576 * @param array $selected_values The selected values to possibly pre-select the <select>.
577 *
578 * @return string
579 */
580 public function get_activity_report_filter_dropdown( $selected_values = array() ) {
581
582 if ( ! is_array( $selected_values ) ) {
583 $selected_values = array( $selected_values );
584 }
585
586 $options_values = apply_filters(
587 'charitable_dashboard_activity_report_type_filter_values',
588 array(
589 'donations-made' => __( 'Donations Made (All)', 'charitable' ),
590 'donations-made-paid' => __( 'Donations Made (Paid)', 'charitable' ),
591 'refunds' => __( 'Refunds', 'charitable' ),
592 'campaigns-created' => __( 'Campaigns Created', 'charitable' ),
593 'user-comments' => __( 'User Comments', 'charitable' ),
594 )
595 );
596
597 ob_start();
598
599 ?>
600
601 <select name="action" id="report-activity-type-filter">
602 <option value=""><?php echo esc_html__( 'All Activity', 'charitable' ); ?> </option>
603 <?php foreach ( $options_values as $value => $label ) : ?>
604 <?php $selected = ! empty( $selected_values ) && in_array( $value, $selected_values, true ) ? 'selected="selected"' : ''; ?>
605 <option value="<?php echo esc_attr( $value ); ?>" <?php echo esc_html( $selected ); ?>><?php echo esc_html( $label ); ?></option>
606 <?php endforeach; ?>
607 </select>
608
609 <?php
610
611 return ob_get_clean();
612 }
613
614 /**
615 * Generate HTML for the Activity List
616 *
617 * @since 1.8.1
618 *
619 * @param array $activities The activity data.
620 * @param bool $include_icons Whether to include icons.
621 * @param string $show_date_value The date value to show when showing "time ago". Default is 'date_to_sort'.
622 *
623 * @return string
624 */
625 public function generate_activity_list( $activities = array(), $include_icons = true, $show_date_value = 'date_to_sort' ) {
626
627 ob_start();
628
629 if ( empty( $activities ) ) {
630 ?>
631
632
633 <p><strong><?php echo esc_html__( 'There are no activity within the date range.', 'charitable' ); ?></strong></p>
634
635
636 <?php } else { ?>
637
638 <ul id="charitable-activity-list" class="charitable-activity-list">
639
640 <?php
641
642 foreach ( $activities as $activity ) :
643
644 $time_ago = ! empty( $activity->$show_date_value ) ? human_time_diff( strtotime( $activity->$show_date_value ), time() ) : false;
645 $item_id = $activity->type === 'campaign' ? $activity->campaign_id : $activity->item_id;
646
647 ?>
648 <li>
649 <?php if ( $include_icons ) : ?>
650 <div class="icon">
651 <img src="<?php echo esc_url( $this->get_activity_icon_url( $activity->icon ) ); ?>" alt="<?php echo esc_html( $activity->status_label ); ?>">
652 </div>
653 <?php endif; ?>
654 <div class="info">
655 <p>
656 <?php
657
658 /* Determine Label */
659
660 // If the user isn't using pro and is on a page that isn't the report overview, give them a generic label.
661 if ( ! charitable_is_pro() && ( ! isset( $_GET['tab'] ) || ( isset( $_GET['tab'] ) && 'overview' !== $_GET['tab'] ) ) ) { // phpcs:ignore
662 $action_label = esc_html__( 'Charitable Donor', 'charitable' );
663 } else {
664 // If this is a campaign, show the campaign name vs just any number ID.
665 $action_label = isset( $activity->type ) && $activity->type === 'campaign' ? str_replace( '%id', '"' . esc_html( $activity->campaign_title ) . '"', $activity->action_label ) : str_replace( '%id', $activity->item_id_prefix . $item_id, $activity->action_label );
666 }
667
668 echo $action_label; // phpcs:ignore
669
670 ?>
671 <span class="badge <?php echo esc_attr( $activity->status ); ?>"><?php echo esc_html( $activity->status_label ); ?></span>
672 </p>
673 <?php
674
675 /* Determine Secondary Info */
676
677 // If this is a campaign, get the created_by value and show a name of who created it.
678 if ( isset( $activity->type ) && $activity->type === 'campaign' && ! empty( $activity->created_by ) ) {
679 $created_by = get_userdata( $activity->created_by );
680 $created_by = $created_by ? $created_by->display_name : '';
681 // get the WordPress user admin url to edit this user.
682 $admin_user_url = get_edit_user_link( $activity->created_by );
683
684 $secondary_info = '<p class="charitable-reports-activity-secondary-info amount">' . esc_html__( 'Created By:', 'charitable' ) . ' <a href="' . esc_url( $admin_user_url ) . '">' . esc_html( $created_by ) . '</a></p>';
685 } else {
686 $secondary_info = $this->get_activity_secondary_info( $activity );
687 }
688
689 echo $secondary_info; // phpcs:ignore
690
691 // phpcs:ignore ?>
692 </div>
693 <?php if ( $time_ago ) : ?>
694 <div class="time-ago">
695 <?php echo esc_html( $time_ago ); ?> <?php echo esc_html__( 'ago', 'charitable' ); ?>
696 </div>
697 <?php endif; ?>
698 <?php
699 if ( ! $include_icons ) :
700 ?>
701 <br class="clear" /><?php endif; ?>
702 </li>
703 <?php endforeach; ?>
704 </ul>
705
706 <?php
707 }
708
709 return ob_get_clean();
710 }
711
712 /**
713 * Generate HTML for the "secondary" info for an activity.
714 *
715 * @since 1.8.1
716 *
717 * @param object $activity The activity data.
718 *
719 * @return string
720 */
721 public function get_activity_secondary_info( $activity ) {
722
723 // Each "activity" needs at least an activity type.
724 if ( empty( $activity ) || empty( $activity->type ) ) {
725 return;
726 }
727
728 switch ( $activity->type ) {
729 case 'donation':
730 return $this->get_donation_activity_secondary_info( $activity );
731 case 'campaign':
732 return $this->get_campaign_activity_secondary_info( $activity );
733 default:
734 return;
735 }
736 }
737
738 /**
739 * Generate HTML for the "secondary" info for a donation activity.
740 *
741 * @since 1.8.1
742 *
743 * @param object $activity The activity data.
744 *
745 * @return string
746 */
747 public function get_donation_activity_secondary_info( $activity ) {
748
749 if ( empty( $activity ) || empty( $activity->type ) ) {
750 return;
751 }
752
753 $secondary_info = '';
754
755 $primary_action = isset( $activity->primary_action ) ? $activity->primary_action : false;
756
757 switch ( $primary_action ) {
758 case 'charitable-completed':
759 case 'charitable-pending':
760 case 'charitable-refunded':
761 case 'charitable-failed':
762 $admin_campaign_url = ! empty( $activity->campaign_id ) ? charitable_get_admin_campaign_edit_url( $activity->campaign_id ) : '#';
763 $admin_donation_url = ! empty( $activity->item_id ) ? charitable_get_admin_donation_edit_url( $activity->item_id ) : false;
764 $campaign_title = ! empty( $activity->campaign_title ) ? ' to <a target="_blank" href="' . $admin_campaign_url . '"><span class="campaign-title">' . esc_html( $activity->campaign_title ) . '</span></a> ' : '';
765 $secondary_info = $admin_donation_url ? '<p class="charitable-reports-activity-secondary-info amount"><a href="' . $admin_donation_url . '" target="_blank">' . charitable_format_money( $activity->amount, 2, true ) . '</a>' . $campaign_title . '</p>' : '<p class="charitable-reports-activity-secondary-info amount">' . charitable_format_money( $activity->amount, 2, true ) . $campaign_title . '</p>';
766 break;
767 default:
768 $admin_campaign_url = ! empty( $activity->campaign_id ) ? charitable_get_admin_campaign_edit_url( $activity->campaign_id ) : '#';
769 $admin_donation_url = ! empty( $activity->item_id ) ? charitable_get_admin_donation_edit_url( $activity->item_id ) : false;
770 $campaign_title = ! empty( $activity->campaign_title ) ? ' to <a target="_blank" href="' . $admin_campaign_url . '"><span class="campaign-title">' . esc_html( $activity->campaign_title ) . '</span></a> ' : '';
771 $secondary_info = $admin_donation_url ? '<p class="charitable-reports-activity-secondary-info amount"><a href="' . $admin_donation_url . '" target="_blank">' . charitable_format_money( $activity->amount, 2, true ) . '</a>' . $campaign_title . '</p>' : '<p class="charitable-reports-activity-secondary-info amount">' . charitable_format_money( $activity->amount, 2, true ) . $campaign_title . '</p>';
772 break;
773 }
774
775 return $secondary_info;
776 }
777
778 /**
779 * Generate HTML for the "secondary" info for a campaign activity.
780 *
781 * @since 1.8.1
782 *
783 * @param object $activity The activity data.
784 *
785 * @return string
786 */
787 public function get_campaign_activity_secondary_info( $activity ) {
788
789 $secondary_info = '';
790
791 switch ( $activity->primary_action ) {
792 case 'update':
793 $secondary_info = '<p class="campaign-title">' . esc_html( $activity->campaign_title ) . '</p>';
794 break;
795 default:
796 $secondary_info = '<p class="campaign-title">' . esc_html( $activity->campaign_title ) . '</p>';
797 break;
798 }
799
800 return $secondary_info;
801 }
802
803 /**
804 * Generates HTML for the payment methods list.
805 *
806 * @since 1.8.1
807 *
808 * @param array $payment_breakdown The payment breakdown data.
809 * @param bool $include_icons Whether to include icons.
810 *
811 * @return string
812 */
813 public function generate_payment_methods_list( $payment_breakdown = array(), $include_icons = true ) {
814
815 ob_start();
816
817 if ( empty( $payment_breakdown ) ) {
818 ?>
819
820 <li class="none">
821 <div class="icon">
822 <span></span>
823 </div>
824 <div class="info">
825 <span><?php echo esc_html__( 'No payments.', 'charitable' ); ?></span>
826 </div>
827 <div class="total">
828 <span>-</span>
829 </div>
830 </li>
831
832 <?php
833 } else {
834
835 foreach ( $payment_breakdown['donations'] as $payment_method => $data ) :
836
837 $percentage = intval( $data['donors'] ) / $payment_breakdown['donations_total'] * 100;
838 // round to nearest .1 number.
839 $percentage = round( $percentage, 1 );
840
841 $title = ! empty( $data['title'] ) ? 'title="' . esc_html( $data['title'] ) . '"' : false;
842
843 ?>
844 <li class="<?php echo esc_attr( $payment_method ); ?>">
845 <?php if ( $include_icons ) : ?>
846 <div class="icon">
847 <span <?php echo $title; // phpcs:ignore ?>></span>
848 </div>
849 <?php endif; ?>
850 <div class="info">
851 <span <?php echo $title; // phpcs:ignore ?>><?php echo esc_html( $data['label'] ); ?></span>
852 </div>
853 <div class="total">
854 <span><?php echo esc_html( $percentage ); ?>%</span>
855 </div>
856 <?php
857 if ( ! $include_icons ) :
858 ?>
859 <br class="clear" /><?php endif; ?>
860 </li>
861 <?php
862 endforeach;
863
864 }
865
866 return ob_get_clean();
867 }
868
869 /**
870 * Generates HTML rows for donation breakdown table.
871 *
872 * @since 1.8.1
873 *
874 * @param array $donation_data The donation data.
875 * @param array $refunds_data The refunds data.
876 *
877 * @return string
878 */
879 public function generate_donations_breakdown_rows( $donation_data = array(), $refunds_data = array() ) {
880
881 ob_start();
882
883 if ( empty( $donation_data ) ) {
884 ?>
885
886 <td colspan="5"><strong><?php echo esc_html__( 'There are no donations within the date range.', 'charitable' ); ?></strong></td>
887
888 <?php
889 } else {
890
891 foreach ( $donation_data as $day => $data ) :
892
893 $net = 0;
894 $refund_count = 0;
895 $css_class = false;
896 $prefix = false;
897 $row_id = strtotime( $day );
898
899 $net = $data['amount'] - $refunds_data['refunds_by_day'][ $day ]['amount'];
900 $prefix = intval( $net ) < 0 ? '-' : '+';
901 $css_class = intval( $net ) < 0 ? 'negative' : 'positive';
902 $css_class = intval( $net ) === 0 ? 'zero' : $css_class;
903 $refund_count = intval( $refunds_data['refunds_by_day'][ $day ]['donors'] );
904 $refund_amount = $refunds_data['refunds_by_day'][ $day ]['amount'];
905
906 ?>
907
908 <tr id="type-donations-donor-<?php echo intval( $row_id ); ?>" class="row-type-donation-breakdown">
909 <td class="donated column-date" data-colname="Date"><?php echo esc_html( $data['label'] ); ?></td>
910 <td class="donated column-donations" data-colname="Donations"><?php echo charitable_format_money( esc_html( $data['amount'] ) ); // phpcs:ignore ?></td>
911 <td class="donated column-donors" data-colname="Number of Donors"><?php echo esc_html( $data['donors'] ); ?></td>
912 <td class="donated column-refunds" data-colname="Refunds"><?php echo charitable_format_money( ( $refund_amount ) ); // phpcs:ignore ?></td>
913 <td class="donated column-net <?php echo $css_class; ?>" data-colname="Net"><?php echo $prefix . charitable_format_money( esc_html( $net ) ); // phpcs:ignore ?></td>
914 </tr>
915
916 <?php
917 endforeach;
918
919 }
920
921 return ob_get_clean();
922 }
923
924 /**
925 * Generates meta info (title, column names) for the report table.
926 *
927 * @since 1.8.1
928 *
929 * @param string $report_type The report type.
930 *
931 * @return array
932 */
933 public function get_donor_report_table_meta( $report_type = 'donors-top' ) {
934
935 $table_meta = array();
936
937 switch ( $report_type ) {
938 case 'donors-top':
939 $table_meta = array(
940 'title' => esc_html__( 'Donors', 'charitable' ),
941 'columns' => array(
942 'avatar' => '',
943 'name-email' => esc_html__( 'Name', 'charitable' ),
944 'total-donations' => esc_html__( 'Total Donations', 'charitable' ),
945 'number-donations' => esc_html__( 'Number of Donations', 'charitable' ),
946 'average' => esc_html__( 'Average', 'charitable' ),
947 'number-campaigns' => esc_html__( 'Number of Campaigns', 'charitable' ),
948 'last-donation' => esc_html__( 'Last Donation', 'charitable' ),
949 'actions' => esc_html__( 'Actions', 'charitable' ),
950 ),
951 );
952 break;
953
954 case 'donors-recurring':
955 $table_meta = array(
956 'title' => esc_html__( 'Donors', 'charitable' ),
957 'columns' => array(
958 'avatar' => '',
959 'name-email' => esc_html__( 'Name', 'charitable' ),
960 'total-donations' => esc_html__( 'Total Donations', 'charitable' ),
961 'number-donations' => esc_html__( 'Number of Donations', 'charitable' ),
962 'average' => esc_html__( 'Average', 'charitable' ),
963 'number-campaigns' => esc_html__( 'Number of Campaigns', 'charitable' ),
964 'last-donation' => esc_html__( 'Last Donation', 'charitable' ),
965 'actions' => esc_html__( 'Actions', 'charitable' ),
966 ),
967 );
968 break;
969
970 case 'donors-first-time':
971 $table_meta = array(
972 'title' => esc_html__( 'Donors', 'charitable' ),
973 'columns' => array(
974 'avatar' => '',
975 'name-email' => esc_html__( 'Name', 'charitable' ),
976 'total-donations' => esc_html__( 'Donation', 'charitable' ),
977 'last-donation' => esc_html__( 'Campaign', 'charitable' ),
978 'date' => esc_html__( 'Date', 'charitable' ),
979 'actions' => esc_html__( 'Actions', 'charitable' ),
980 ),
981 );
982 break;
983
984 default:
985 // code...
986 break;
987 }
988
989 return $table_meta;
990 }
991
992 /**
993 * Generates HTML for the donor breakdown table on the overview page.
994 *
995 * @since 1.8.1
996 * @since 1.8.3 Added $limit_per_page parameter and generate_pagination_html().
997 *
998 * @param string $report_type The report type.
999 * @param array $donor_breakdown The donor breakdown data.
1000 * @param int $current_page The current page.
1001 * @param int $limit_per_page The limit per page.
1002 *
1003 * @return string
1004 */
1005 public function generate_donor_breakdown_table_html( $report_type = 'donors-top', $donor_breakdown = array(), $current_page = 1, $limit_per_page = 10 ) {
1006
1007 $table_meta = $this->get_donor_report_table_meta( $report_type );
1008
1009 ob_start();
1010
1011 ?>
1012
1013 <div class="alignleft actions">
1014
1015 <h2><?php echo esc_html( $table_meta['title'] ); ?></h2>
1016
1017 </div>
1018 <div class="alignright">
1019
1020 <?php if ( ! charitable_is_pro() ) : ?>
1021
1022 <button disabled="disabled" value="<?php echo esc_html__( 'Download CSV', 'charitable' ); ?>" class="button with-icon charitable-report-download-button" title="<?php echo esc_html__( 'Download CSV', 'charitable' ); ?>" data-nonce=""><label><?php echo esc_html__( 'Download CSV', 'charitable' ); ?></label><img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) . 'images/icons/download.svg' ); ?>" alt=""></button>
1023
1024 <?php else : ?>
1025
1026 <form action="" method="post" class="charitable-report-donor-form" id="charitable-donor-download-form">
1027 <input name="charitable_report_action" type="hidden" value="charitable_report_download_donors">
1028 <input name="report_type" type="hidden" value="<?php echo esc_attr( $report_type ); ?>">
1029 <?php wp_nonce_field( 'charitable_export_report', 'charitable_export_report_nonce' ); ?>
1030 <button value="<?php echo esc_html__( 'Download CSV', 'charitable' ); ?>" type="submit" class="button with-icon charitable-report-download-button" title="<?php echo esc_html__( 'Download CSV', 'charitable' ); ?>" data-nonce=""><label><?php echo esc_html__( 'Download CSV', 'charitable' ); ?></label><img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) . 'images/icons/download.svg' ); ?>" alt=""></button>
1031 </form>
1032
1033 <?php endif; ?>
1034
1035 </div>
1036
1037 <br class="clear">
1038
1039 <div class="charitable-report-table-container">
1040
1041 <?php do_action( 'charitable_report_before_donor_table' ); ?>
1042
1043 <?php
1044 if ( ! charitable_is_pro() ) :
1045 ?>
1046 <div class="charitable-restricted"><div class="restricted-access-overlay"></div><?php endif; ?>
1047
1048 <table class="wp-list-table widefat fixed striped table-view-list donor-breakdown charitable-report-ui">
1049 <thead>
1050 <tr>
1051 <?php
1052 foreach ( $table_meta['columns'] as $column_slug => $column_title ) :
1053 if ( $column_slug === 'avatar' ) {
1054 echo '<th style="width: 32px;" class="charitable-avatar"></th>';
1055 } else {
1056 echo '<th scope="col" id="' . esc_attr( $column_slug ) . '" class="manage-column column-' . esc_attr( $column_slug ) . '"><span>' . esc_html( $column_title ) . '</span></th>';
1057 }
1058 endforeach;
1059 ?>
1060 </tr>
1061 </thead>
1062 <tbody id="donations-breakdown-list">
1063 <?php echo $this->generate_donor_breakdown_rows( $donor_breakdown, $report_type ); // phpcs:ignore ?>
1064 </tbody>
1065 <tfoot>
1066 <tr>
1067 <?php
1068 foreach ( $table_meta['columns'] as $column_slug => $column_title ) :
1069 if ( $column_slug === 'avatar' ) {
1070 echo '<th style="width: 32px;" class="charitable-avatar"></th>';
1071 } else {
1072 echo '<th scope="col" id="' . esc_html( $column_slug ) . '" class="manage-column column-' . esc_attr( $column_slug ) . '"><span>' . esc_html( $column_title ) . '</span></th>';
1073 }
1074 endforeach;
1075 ?>
1076 </tr>
1077 </tfoot>
1078 </table>
1079
1080 <?php
1081 if ( ! charitable_is_pro() ) :
1082 ?>
1083 </div><?php endif; ?>
1084
1085 <?php do_action( 'charitable_report_after_donor_table' ); ?>
1086
1087 <br class="clear">
1088
1089 <?php echo $this->generate_pagination_html( $limit_per_page, $current_page, $report_type ); // phpcs:ignore ?>
1090
1091 <?php do_action( 'charitable_report_after_donor_table_pagination' ); ?>
1092
1093 </div>
1094
1095 <?php
1096
1097 return ob_get_clean();
1098 }
1099
1100 /**
1101 * Generates HTML for the donation breakdown table on the overview page.
1102 *
1103 * @since 1.8.1
1104 *
1105 * @param array $donor_data The donor data.
1106 * @param string $report_type The report type.
1107 *
1108 * @return string
1109 */
1110 public function generate_donor_breakdown_rows( $donor_data = array(), $report_type = 'donors-top' ) {
1111
1112 $suffix = ( charitable_is_debug() || charitable_is_script_debug() ) ? '#charitabledebug' : false;
1113
1114 ob_start();
1115
1116 if ( empty( $donor_data ) ) {
1117 ?>
1118
1119 <td colspan="5"><strong><?php echo esc_html__( 'There are no donors found.', 'charitable' ); ?></strong></td>
1120
1121 <?php
1122
1123 } else {
1124
1125 foreach ( $donor_data as $index => $data ) :
1126
1127 $donor_id = isset( $data->donor_id ) ? intval( $data->donor_id ) : 0;
1128 $donor_email = isset( $data->email ) ? esc_html( $data->email ) : '';
1129 $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . 'images/misc/placeholder-avatar-small.jpg';
1130 $donor_name = esc_html( $data->first_name ) . ' ' . esc_html( $data->last_name );
1131 $donor_number_donations = isset( $data->total_count_donations ) ? intval( $data->total_count_donations ) : 0;
1132 $donor_number_campaigns = isset( $data->total_count_campaigns ) ? intval( $data->total_count_campaigns ) : 0;
1133 $donor_average = $donor_number_donations > 0 ? $data->total_amount / $donor_number_donations : 0;
1134 $donation_id = isset( $data->donation_id ) ? intval( $data->donation_id ) : 0;
1135 $is_sample_data = isset( $data->sample ) ? true : false;
1136
1137 // date format: M j, Y plus time of day in am/pm.
1138 $last_donation_date = $data->last_donation_date ? gmdate( 'M j, Y g:i a', strtotime( $data->last_donation_date ) ) : 'N/A';
1139 $last_donation_campaign_name = $data->last_campaign_title ? esc_html( $data->last_campaign_title ) : 'N/A';
1140 $last_donation_campaign_id = $data->last_campaign_id ? intval( $data->last_campaign_id ) : 0;
1141
1142 $action_link = '#';
1143 $action_label = false;
1144
1145 // Determine the (view) link.
1146 if ( 'donors-first-time' === $report_type && $donation_id > 0 ) {
1147
1148 $action_link = ( $donor_id !== 0 ) ? admin_url( 'post.php?post=' . $donation_id . '&action=edit' ) : '#';
1149 $action_label = esc_html__( 'View Donation', 'charitable' );
1150
1151 } elseif ( $last_donation_campaign_id > 0 ) {
1152
1153 // Does this post exist?
1154 if ( false === get_post_status( $last_donation_campaign_id ) ) {
1155
1156 $action_link = '#';
1157
1158 } else {
1159
1160 // Determine if this is a "legacy" campaign.
1161 $is_legacy_campaign = charitable_is_campaign_legacy( $last_donation_campaign_id );
1162
1163 $action_link = $is_legacy_campaign ? admin_url( 'post.php?post=' . $last_donation_campaign_id . '&action=edit' ) : admin_url( 'admin.php?page=charitable-campaign-builder&campaign_id=' . $last_donation_campaign_id ) . $suffix;
1164 $action_label = 'donors-top' === $report_type ? esc_html__( 'View Last Donated Campaign', 'charitable' ) : esc_html__( 'View Campaign', 'charitable' );
1165
1166 }
1167 }
1168
1169 if ( 'donors-first-time' === $report_type ) {
1170 if ( $last_donation_campaign_name && $last_donation_campaign_id ) {
1171 $last_donation_html = '<a href="' . $action_link . '" target="_blank">' . $last_donation_campaign_name . '</a>';
1172 } elseif ( $is_sample_data ) {
1173 $last_donation_html = $last_donation_campaign_name;
1174 }
1175 } elseif ( $last_donation_date && $last_donation_campaign_name && $last_donation_campaign_id ) {
1176 $last_donation_html = charitable_format_money( esc_html( $data->amount ), 2, true ) . '<br/><a href="' . $action_link . '" target="_blank">' . $last_donation_campaign_name . '</a><br/>' . $last_donation_date;
1177 } elseif ( $is_sample_data ) {
1178 $last_donation_html = charitable_format_money( esc_html( $data->amount ), 2, true ) . '<br/>' . $last_donation_campaign_name . '<br/>' . $last_donation_date;
1179 }
1180
1181 if ( $report_type === 'donors-first-time' ) {
1182 ?>
1183
1184 <tr id="type-<?php echo esc_attr( $report_type ); ?>-donor-<?php echo intval( $donor_id ); ?>" class="type-<?php echo esc_attr( $report_type ); ?>">
1185 <td class="charitable-avatar" style="width: 32px;"><img src="<?php echo esc_url( $donor_avatar ); ?>" class="avatar avatar-32 photo charitable-avatar-donor-<?php echo intval( $donor_id ); ?>" width="32" height="32" alt="<?php echo esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>"></td>
1186 <td class="donated column-date" data-colname="<?php echo esc_html__( 'Name / Email', 'charitable' ); ?>"><?php echo esc_html( $donor_name ); ?><br/><?php echo esc_html( $donor_email ); ?></td>
1187 <td class="donated column-donation-amount" data-colname="<?php echo esc_html__( 'Donation Amount', 'charitable' ); ?>"><?php echo charitable_format_money( esc_html( $data->total_amount ), 2, true ); // phpcs:ignore ?></td>
1188 <td class="donated column-campaign" data-colname="<?php echo esc_html__( 'Campaign', 'charitable' ); ?>"><?php echo $last_donation_html; // phpcs:ignore ?></td>
1189 <td class="donated column-date" data-colname="<?php echo esc_html__( 'Date', 'charitable' ); ?>"><?php echo esc_html( $last_donation_date ); ?></td>
1190 <?php
1191 $display = '<td class="donated column-actions " data-colname="Actions">';
1192 if ( $action_label !== false ) {
1193 $display .= '<a class="charitable-campaign-action-button" title="' . $action_label . '" href="' . esc_url( $action_link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . 'images/icons/eye.svg" width="14" height="14" alt="' . $action_label . '" /></a>';
1194 }
1195 $display .= '</td>';
1196
1197 echo $display; // phpcs:ignore
1198 ?>
1199 </tr>
1200
1201 <?php } else { ?>
1202
1203 <tr id="type-<?php echo esc_attr( $report_type ); ?>-donor-<?php echo intval( $donor_id ); ?>" class="type-<?php echo esc_attr( $report_type ); ?>">
1204 <td class="charitable-avatar" style="width: 32px;"><img src="<?php echo esc_url( $donor_avatar ); ?>" class="avatar avatar-32 photo charitable-avatar-donor-<?php echo intval( $donor_id ); ?>" width="32" height="32" alt="<?php echo esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>"></td>
1205 <td class="donated column-date" data-colname="<?php echo esc_html__( 'Name / Email', 'charitable' ); ?>"><?php echo esc_html( $donor_name ); ?><br/><?php echo esc_html( $donor_email ); ?></td>
1206 <td class="donated column-total-donations" data-colname="<?php echo esc_html__( 'Total Donations', 'charitable' ); ?>"><?php echo charitable_format_money( esc_html( $data->total_amount ), 2, true ); // phpcs:ignore ?></td>
1207 <td class="donated column-number-donations" data-colname="Number of Donations"><?php echo esc_html( $donor_number_donations ); ?></td>
1208 <td class="donated column-average" data-colname="Average"><?php echo charitable_format_money( $donor_average, 2, true ); // phpcs:ignore ?></td>
1209 <td class="donated column-number-campaigns" data-colname="Number of Campaigns"><?php echo esc_html( $donor_number_campaigns ); ?></td>
1210 <td class="donated column-last-donation" data-colname="Last Donation"><?php echo $last_donation_html; // phpcs:ignore ?></td>
1211 <?php
1212 $display = '<td class="donated column-actions" data-colname="Actions">';
1213 if ( $action_label !== false ) {
1214 $display .= '<a class="charitable-campaign-action-button" title="' . $action_label . '" href="' . esc_url( $action_link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . 'images/icons/eye.svg" width="14" height="14" alt="' . $action_label . '" /</a>';
1215 }
1216 $display .= '</td>';
1217
1218 echo $display; // phpcs:ignore
1219 ?>
1220 </tr>
1221
1222 <?php
1223 }
1224
1225 endforeach;
1226
1227 }
1228
1229 return ob_get_clean();
1230 }
1231
1232 /**
1233 * Enqueue assets and write JS vars for the report.
1234 *
1235 * @since 1.8.1
1236 *
1237 * @return void
1238 */
1239 public function report_vars() {
1240
1241 $localized_strings = $this->get_localized_strings();
1242
1243 ?>
1244 <script id="charitable-report-data-js">
1245 var charitable_reporting = <?php echo wp_json_encode( $localized_strings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); ?>;
1246 </script>
1247
1248 <?php
1249 }
1250
1251 /**
1252 * Get the donations stored in the class.
1253 *
1254 * @since 1.8.1
1255 *
1256 * @return array
1257 */
1258 public function get_donations() {
1259 return $this->donations;
1260 }
1261
1262 /**
1263 * Get the donations stored in the class, and return sorted/organized by payment method.
1264 *
1265 * @since 1.8.1
1266 *
1267 * @return array
1268 */
1269 public function get_donations_by_payment() {
1270 if ( empty( $this->donations ) ) {
1271 return;
1272 }
1273
1274 // setup array so that it holds all the days in the date range.
1275 $donations_by_payment = apply_filters(
1276 'charitable_reports_default_payment_types',
1277 array(
1278 'stripe' => array(
1279 'label' => esc_html__( 'Stripe', 'charitable' ),
1280 'amount' => 0,
1281 'donors' => 0,
1282 'title' => esc_html__( 'Donations made through the Stripe gateway.', 'charitable' ),
1283 ),
1284 'paypal' => array(
1285 'label' => esc_html__( 'PayPal', 'charitable' ),
1286 'amount' => 0,
1287 'donors' => 0,
1288 'title' => esc_html__( 'Donations made through the PayPal gateway.', 'charitable' ),
1289 ),
1290 'manual' => array(
1291 'label' => esc_html__( 'Manual', 'charitable' ),
1292 'amount' => 0,
1293 'donors' => 0,
1294 'title' => esc_html__( 'Donations manually entered into the WordPress admin.', 'charitable' ),
1295 ),
1296 'offline' => array(
1297 'label' => esc_html__( 'Offline', 'charitable' ),
1298 'amount' => 0,
1299 'donors' => 0,
1300 'title' => esc_html__( 'Donations made outside of the website.', 'charitable' ),
1301 ),
1302 )
1303 );
1304
1305 // Go through the array and group the amounts and number of donors by day.
1306 foreach ( $this->donations as $key => $donation ) {
1307
1308 $payment = ( ! empty( $donation->payment_gateway ) ? strtolower( esc_attr( $donation->payment_gateway ) ) : $payment = 'unknown' );
1309
1310 // Get the proper label from the gateway object if available.
1311 $label = '';
1312 $gateways = charitable_get_helper( 'gateways' );
1313 if ( $gateways && $gateways->is_valid_gateway( $payment ) ) {
1314 $gateway_object = $gateways->get_gateway_object( $payment );
1315 if ( $gateway_object ) {
1316 $label = $gateway_object->get_label();
1317 }
1318 }
1319
1320 // Fallback to manual label generation if gateway object is not available.
1321 if ( empty( $label ) ) {
1322 // cap the P in Paypal manually.
1323 $label = ( 'paypal' === strtolower( $donation->payment_gateway ) ) ? 'PayPal' : ucwords( str_replace( '_', ' ', $donation->payment_gateway ) );
1324 }
1325
1326 if ( isset( $donations_by_payment[ $payment ] ) ) {
1327 // Update the label if it wasn't set in the default array.
1328 if ( ! empty( $label ) ) {
1329 $donations_by_payment[ $payment ]['label'] = esc_html( $label );
1330 }
1331 $donations_by_payment[ $payment ]['amount'] += $donation->total_amount;
1332 $donations_by_payment[ $payment ]['donors'] += 1;
1333 } else {
1334 $donations_by_payment[ $payment ]['label'] = esc_html( $label );
1335 $donations_by_payment[ $payment ]['amount'] = $donation->total_amount;
1336 $donations_by_payment[ $payment ]['donors'] = 1;
1337 $donations_by_payment[ $payment ]['title'] = $donation->title;
1338 }
1339 }
1340
1341 $this->payment_percentages = array();
1342 $this->payment_labels = array();
1343 $this->payment_keys = array();
1344
1345 foreach ( $donations_by_payment as $key => $donation ) {
1346
1347 $percentage = intval( $donation['donors'] ) / count( $this->donations ) * 100;
1348 // round to nearest .1 number.
1349 $percentage = round( $percentage, 1 );
1350
1351 $this->payment_percentages[] = $percentage;
1352 $this->payment_labels[] = $donation['label']; // . '% (' . charitable_format_money( $donation['amount'] ) . ')';
1353 $this->payment_keys[] = $key; // Store the payment gateway key for color mapping.
1354
1355 }
1356
1357 if ( empty( $this->payment_labels ) && empty( $this->payment_percentages ) ) {
1358 // no donations were likely found so let's add the "not found" data.
1359 $this->payment_percentages[] = 100;
1360 $this->payment_labels[] = __( 'No donations found.', 'charitable' );
1361 $this->payment_keys[] = 'none';
1362 } else {
1363 $this->payment_percentages[] = 0;
1364 $this->payment_labels[] = '';
1365 $this->payment_keys[] = '';
1366 }
1367
1368 $donations_by_payment = apply_filters( 'charitable_reports_donations_by_payment_data', $donations_by_payment );
1369
1370 return array(
1371 'donations' => $donations_by_payment,
1372 'donations_total' => count( $this->donations ),
1373 );
1374 }
1375
1376
1377 /**
1378 * Get unique donor count from donations.
1379 *
1380 * @since 1.8.1
1381 *
1382 * @return int
1383 */
1384 public function get_donors_count() {
1385 if ( empty( $this->donations ) ) {
1386 return 0;
1387 }
1388
1389 // Go through multi-dimenssional array and get a count of all unique values of donor_id.
1390 $donor_ids = array();
1391 $donation_total = 0;
1392
1393 foreach ( $this->donations as $donation ) {
1394 // Add the donor ID to the array.
1395 $donor_ids[] = $donation->donor_id;
1396 // Add the donation amount to the total.
1397 $donation_total += $donation->total_amount;
1398 }
1399
1400 return ( count( array_unique( $donor_ids ) ) );
1401 }
1402
1403 /**
1404 * Get donation amount total from donations.
1405 *
1406 * @since 1.8.1
1407 *
1408 * @return float
1409 */
1410 public function get_donations_total() {
1411 if ( empty( $this->donations ) ) {
1412 return 0;
1413 }
1414
1415 // Go through multi-dimenssional array and get a count of all unique values of donor_id.
1416 $donation_total = 0;
1417
1418 foreach ( $this->donations as $donation ) {
1419 // Add the donation amount to the total.
1420 $donation_total += $donation->total_amount;
1421 }
1422
1423 return ( (float) $donation_total );
1424 }
1425
1426 /**
1427 * Get a breakdown of donations per day based on the start and end dates.
1428 *
1429 * @since 1.8.1
1430 *
1431 * @param bool $format_money Whether to format the money (say for downloadable reports ).
1432 *
1433 * @return array
1434 */
1435 public function get_donations_by_day( $format_money = false ) {
1436 if ( empty( $this->donations ) ) {
1437 return;
1438 }
1439
1440 $campaign_donations_table = charitable_get_table( 'campaign_donations' );
1441
1442 // setup array so that it holds all the days in the date range.
1443 $donations_by_day = array();
1444 $days = $this->get_days_between_dates( $this->start_date, $this->end_date );
1445 $refunded_statuses = array( 'charitable-refunded' );
1446
1447 /**
1448 * Two grouped queries for the whole range, rather than two queries per calendar
1449 * day. The old per-day loop made the cost a function of how long the organisation
1450 * had existed rather than how much data it had, so a 24-year range issued ~18,000
1451 * queries and hit max_execution_time before returning anything.
1452 *
1453 * The range is taken from $days rather than from $this->start_date /
1454 * $this->end_date, because get_days_between_dates() normalises a falsy date to
1455 * "now". Reading the bounds back off $days guarantees the query covers exactly
1456 * the days this loop is about to render, however those dates were normalised.
1457 */
1458 $day_keys = array_keys( $days );
1459 $range_start = reset( $day_keys );
1460 $range_end = end( $day_keys );
1461
1462 $completed_by_day = $campaign_donations_table->get_donations_summary_by_day_range( $range_start, $range_end, array( 'charitable-completed', 'charitable-refunded' ) );
1463 $refunded_by_day = $campaign_donations_table->get_donations_summary_by_day_range( $range_start, $range_end, $refunded_statuses );
1464
1465 foreach ( $days as $day_unformatted => $day_label ) {
1466
1467 $donation_day = gmdate( 'Y-m-d', strtotime( $day_unformatted ) );
1468
1469 $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day_unformatted ) );
1470 $donations_by_day[ $donation_day ]['amount'] = isset( $completed_by_day[ $donation_day ] ) ? $completed_by_day[ $donation_day ]->amount : 0;
1471 $donations_by_day[ $donation_day ]['donors'] = 0;
1472 $donations_by_day[ $donation_day ]['refunds'] = isset( $refunded_by_day[ $donation_day ] ) ? $refunded_by_day[ $donation_day ]->amount : 0;
1473 $donations_by_day[ $donation_day ]['net'] = 0;
1474 }
1475
1476 // Go through the array and group the amounts and number of donors by day.
1477 foreach ( $this->donations as $donation ) {
1478
1479 $donation_day = gmdate( 'Y-m-d', strtotime( $donation->post_date ) );
1480
1481 if ( ! empty( $donation->email ) ) {
1482 if ( isset( $unique_donors[ $donation_day ][ $donation->email ] ) ) {
1483 $unique_donors[ $donation_day ][ $donation->email ] = $unique_donors[ $donation_day ][ $donation->email ] + 1;
1484 } else {
1485 $unique_donors[ $donation_day ][ $donation->email ] = 1;
1486 }
1487 }
1488
1489 if ( isset( $donations_by_day[ $donation_day ] ) ) {
1490 $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $donation->post_date ) );
1491 // $donations_by_day[ $donation_day ]['refunds'] += in_array( $donation->donation_status, $refunded_statuses, true ) ? $donation->total_amount : 0;
1492 } else {
1493 $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $donation->post_date ) );
1494 // $donations_by_day[ $donation_day ]['refunds'] = in_array( $donation->donation_status, $refunded_statuses, true ) ? $donation->total_amount : 0;
1495 }
1496 }
1497
1498 // Go through the array and (1) add the net values now that the totals are calculated (2) format amount and refunds (3) add unique donors.
1499 foreach ( $donations_by_day as $day => $data ) {
1500
1501 $donations_by_day[ $day ]['net'] = $this->get_net( $data['amount'], $data['refunds'] );
1502 $donations_by_day[ $day ]['donors'] = ! empty( $unique_donors[ $day ] ) ? count( $unique_donors[ $day ] ) : 0;
1503
1504 if ( $format_money ) :
1505 $donations_by_day[ $day ]['amount'] = $this->charitable_reports_format_money( $data['amount'] );
1506 $donations_by_day[ $day ]['refunds'] = $this->charitable_reports_format_money( $data['refunds'] );
1507 $donations_by_day[ $day ]['net'] = $this->charitable_reports_format_money( $donations_by_day[ $day ]['net'] );
1508 endif;
1509 }
1510
1511 // reverse array.
1512 $donations_by_day = array_reverse( $donations_by_day );
1513
1514 return $donations_by_day;
1515 }
1516
1517 /**
1518 * Get the total amount of refunds from the stored activities.
1519 *
1520 * @since 1.8.1
1521 *
1522 * @param int $campaign_id The campaign ID.
1523 * @param int $limit The limit of activities to get.
1524 *
1525 * @return float
1526 */
1527 public function get_activity_refund_total( $campaign_id = false, $limit = false ) {
1528
1529 if ( $this->is_activities_empty() ) {
1530 $this->activities = $this->get_activity( $campaign_id, $limit );
1531 }
1532
1533 if ( $this->is_activities_empty() ) {
1534 return 0;
1535 }
1536
1537 $refunded_total = 0;
1538
1539 foreach ( $this->activities as $activity ) {
1540 if ( 'refunded' === $activity->status ) {
1541 $refunded_total += $activity->amount;
1542 }
1543 }
1544
1545 return $refunded_total;
1546 }
1547
1548 /**
1549 * Get the total count of refunds from the stored activities.
1550 *
1551 * @since 1.8.1
1552 *
1553 * @param int $campaign_id The campaign ID.
1554 * @param int $limit The limit of activities to get.
1555 *
1556 * @return int
1557 */
1558 public function get_activity_refund_count( $campaign_id = false, $limit = false ) {
1559
1560 if ( $this->is_activities_empty() ) {
1561 $this->activities = $this->get_activity( $campaign_id, $limit );
1562 }
1563
1564 if ( $this->is_activities_empty() ) {
1565 return 0;
1566 }
1567
1568 $refunded_count = 0;
1569
1570 foreach ( $this->activities as $activity ) {
1571 if ( 'refunded' === $activity->status ) {
1572 ++$refunded_count;
1573 }
1574 }
1575
1576 return $refunded_count;
1577 }
1578
1579 /**
1580 * Get activity for the report.
1581 *
1582 * @since 1.8.1
1583 *
1584 * @param array $args The array of args.
1585 *
1586 * @return array
1587 */
1588 public function get_activity( $args = array() ) {
1589
1590 $defaults = array(
1591 'activity_filter_types' => array(),
1592 'activity_action_types' => array(),
1593 'start_date' => false,
1594 'end_date' => false,
1595 'status' => false,
1596 'campaign_id' => false,
1597 'category_id' => false,
1598 'limit' => false,
1599 'offset' => false,
1600 'ppage' => false,
1601 );
1602
1603 $args = wp_parse_args( $args, $defaults );
1604
1605 // Extract individual variables from args array.
1606 $activity_filter_types = $args['activity_filter_types'] ?? array();
1607 $activity_action_types = $args['activity_action_types'] ?? array();
1608 $start_date = $args['start_date'] ?? false;
1609 $end_date = $args['end_date'] ?? false;
1610 $status = $args['status'] ?? false;
1611 $campaign_id = $args['campaign_id'] ?? false;
1612 $category_id = $args['category_id'] ?? false;
1613 $limit = $args['limit'] ?? false;
1614 $offset = $args['offset'] ?? false;
1615 $ppage = $args['ppage'] ?? false;
1616
1617 if ( ! class_exists( 'Charitable_Admin_Activities' ) || ! class_exists( 'Charitable_Donation_Activities_DB' ) || ! class_exists( 'Charitable_Campaign_Activities_DB' ) ) {
1618 return;
1619 }
1620
1621 $charitable_donation_activities_db = Charitable_Donation_Activities_DB::get_instance();
1622
1623 // check to see if db tables exist.
1624 if ( ! $charitable_donation_activities_db->table_exists() ) {
1625 return;
1626 }
1627
1628 $charitable_campaign_activities_db = Charitable_Campaign_Activities_DB::get_instance();
1629
1630 // check to see if db tables exist.
1631 if ( ! $charitable_campaign_activities_db->table_exists() ) {
1632 return;
1633 }
1634
1635 // If not passed, grab from the local vars in the class.
1636 $campaign_id = $campaign_id ? $campaign_id : $this->campaign_id;
1637 $category_id = absint( $category_id ) > 0 ? $category_id : $this->category_id;
1638
1639 $admin_activities = new Charitable_Admin_Activities();
1640
1641 // Get the "raw" activities data.
1642 $activities = $admin_activities->get_activity(
1643 array(
1644 'activity_filter_types' => $activity_filter_types,
1645 'activity_action_types' => $activity_action_types,
1646 'campaign_id' => $campaign_id,
1647 'category_id' => $category_id,
1648 'start_date' => $this->start_date,
1649 'end_date' => $this->end_date,
1650 'limit' => $limit,
1651 'offset' => $offset,
1652 )
1653 );
1654
1655 // Get the more "formatted" (with additional data) reports data.
1656 $report_activities = $admin_activities->get_activity_report_data( $activities );
1657
1658 return $report_activities;
1659 }
1660
1661 /**
1662 * Generate Pagination HTML
1663 *
1664 * @since 1.8.1
1665 *
1666 * @param int $total_pages The total number of pages.
1667 * @param int $current_page The current page.
1668 * @param int $total_records Total number of records.
1669 * @param int $offset The offset.
1670 * @param int $limit The limit.
1671 * @param array $arr_params The params for the pagination links, which might depend on report type.
1672 * @param int $dot_threshold The dot threshold.
1673 *
1674 * @return void
1675 */
1676 public function show_pagination( $total_pages = 1, $current_page = 1, $total_records = false, $offset = false, $limit = 10, $arr_params = array(), $dot_threshold = false ) {
1677
1678 // Only display if pro.
1679 if ( ! charitable_is_pro() ) {
1680 return;
1681 }
1682
1683 if ( false === $dot_threshold ) {
1684 $dot_threshold = $this->pagination_dot_threshold;
1685 }
1686
1687 $total_pages = intval( $total_pages );
1688
1689 echo '<nav class="charitable-reports-pagination-nav">';
1690
1691 if ( $total_records ) :
1692 // translators: %d: total number of records.
1693 echo '<div class="page-item total-count"><span class="page-total">' . sprintf( esc_html__( 'Total: %d', 'charitable' ), intval( $total_records ) ) . '</span></div>';
1694 endif;
1695
1696 // If there are no pages, don't display anything further.
1697 if ( ! $total_pages || $total_pages <= 1 ) {
1698 echo '</nav>';
1699 return;
1700 }
1701
1702 echo '<ul>';
1703
1704 if ( intval( $total_pages ) > intval( $dot_threshold ) ) {
1705 if ( intval( $current_page ) === 1 ) {
1706 echo '<li class="page-item disabled"><span class="page-link">‹</span></li>';
1707 echo '<li class="page-item active" aria-current="page"><span class="page-link">1</span></li>';
1708 } else {
1709 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( intval( $current_page ) - 1, $arr_params ) ) . '">‹</a></li>';
1710 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( 1 ) ) . '">1</a></li>';
1711 }
1712
1713 if ( $total_pages - $current_page > 3 ) {
1714 if ( intval( $current_page ) > 4 ) {
1715 echo '<li class="page-item"><span class="page-link">...</span></li>';
1716 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( intval( $current_page ) - 1, $arr_params ) ) . '">' . ( intval( $current_page ) - 1 ) . '</a></li>';
1717 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . ( intval( $current_page ) ) . '</span></li>';
1718 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( intval( $current_page ) + 1, $arr_params ) ) . '">' . ( intval( $current_page ) + 1 ) . '</a></li>';
1719 } else {
1720 for ( $page_no = 2; $page_no <= 5; $page_no++ ) {
1721 if ( intval( $current_page ) === $page_no ) {
1722 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . ( intval( $page_no ) ) . '</span></li>';
1723 } else {
1724 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( $page_no, $arr_params ) ) . '">' . intval( $page_no ) . '</a></li>';
1725 }
1726 }
1727 }
1728 }
1729
1730 if ( $total_pages - $current_page < 4 ) {
1731 echo '<li class="page-item"><span class="page-link">...</span></li>';
1732 for ( $page_no = $total_pages - 4; $page_no <= $total_pages - 1; $page_no++ ) {
1733 if ( intval( $current_page ) === $page_no ) {
1734 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . intval( $page_no ) . '</span></li>';
1735 } else {
1736 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( $page_no, $arr_params ) ) . '">' . intval( $page_no ) . '</a></li>';
1737 }
1738 }
1739 } else {
1740 echo '<li class="page-item"><span class="page-link">...</span></li>';
1741 }
1742
1743 if ( intval( $current_page ) === intval( $total_pages ) ) {
1744 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . ( intval( $total_pages ) ) . '</span></li>';
1745 echo '<li class="page-item disabled"><span class="page-link">›</span></li>';
1746 } else {
1747 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( $total_pages, $arr_params ) ) . '">' . ( esc_html( $total_pages ) ) . '</a></li>';
1748 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( intval( $current_page ) + 1, $arr_params ) ) . '">›</a></li>';
1749 }
1750 } else {
1751 if ( intval( $current_page ) === 1 ) {
1752 echo '<li class="page-item disabled"><span class="page-link">‹</span></li>';
1753 } else {
1754 echo '<li class="page-item"><a class="page-link" ' . $this->get_pagination_data_attributes( $total_pages, $current_page, $total_records, $offset, intval( $current_page - 1 ), $limit, $arr_params ) . ' href="' . esc_url( $this->get_pagination_url( intval( $current_page ) - 1, $arr_params ) ) . '">‹</a></li>'; // phpcs:ignore
1755 }
1756 for ( $page_no = 1; $page_no <= $total_pages; $page_no++ ) {
1757 if ( intval( $current_page ) === $page_no ) {
1758 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . intval( $page_no ) . '</span></li>';
1759 } else {
1760 echo '<li class="page-item"><a class="page-link" ' . $this->get_pagination_data_attributes( $total_pages, $current_page, $total_records, $offset, $page_no, $limit, $arr_params ) . ' href="' . esc_url( $this->get_pagination_url( $page_no, $arr_params ) ) . '">' . intval( $page_no ) . '</a></li>'; // phpcs:ignore
1761 }
1762 }
1763 if ( intval( $current_page ) === intval( $total_pages ) ) {
1764 echo '<li class="page-item disabled"><span class="page-link">›</span></li>';
1765 } else {
1766 echo '<li class="page-item"><a class="page-link" ' . $this->get_pagination_data_attributes( $total_pages, $current_page, $total_records, $offset, intval( $current_page + 1 ), $limit, $arr_params ) . ' href="' . esc_url( $this->get_pagination_url( intval( $current_page ) + 1, $arr_params ) ) . '">›</a></li>'; // phpcs:ignore
1767 }
1768 }
1769
1770 echo '</ul></nav>';
1771 }
1772
1773 /**
1774 * Get link for the charitable pagination
1775 *
1776 * @since 1.8.1.3
1777 *
1778 * @param string $total_pages Current page number.
1779 * @param string $current_page Current page.
1780 * @param string $total_records Total number of records.
1781 * @param string $offset The offset.
1782 * @param string $ppage The ppage.
1783 * @param string $limit The limit.
1784 * @param array $arr_params Params to build the attributes.
1785 *
1786 * @return string
1787 */
1788 public function get_pagination_data_attributes( $total_pages = '', $current_page = '', $total_records = 0, $offset = 0, $ppage = 1, $limit = 10, $arr_params = array() ) {
1789
1790 $data_attributes = array(
1791 'total-pages' => $total_pages,
1792 'current-page' => $current_page,
1793 'total-records' => $total_records,
1794 // 'offset' => $offset,
1795 'ppage' => $ppage, // a page in the results.
1796 'limit' => $limit, // the limit of items per page.
1797 );
1798
1799 if ( ! empty( $arr_params ) ) {
1800 foreach ( $arr_params as $key => $value ) {
1801 $data_attributes[ sanitize_title( $key ) ] = $value;
1802 }
1803 }
1804
1805 // create a string of data attributes for an HTML element based on $data_attributes.
1806 $data_attributes_string = '';
1807 foreach ( $data_attributes as $key => $value ) {
1808 $data_attributes_string .= ' data-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"';
1809 }
1810
1811 return $data_attributes_string;
1812 }
1813
1814 /**
1815 * Get link for the charitable pagination
1816 *
1817 * @since 1.8.1
1818 *
1819 * @param string $page_number Current page number.
1820 * @param array $arr_params Params to build the URL.
1821 *
1822 * @return array
1823 */
1824 public function get_pagination_url( $page_number, $arr_params = array() ) {
1825
1826 $arr_params['ppage'] = $page_number;
1827
1828 return add_query_arg( $arr_params, admin_url( 'admin.php' ) );
1829 }
1830
1831 /**
1832 * Generate the HTML for the pagination.
1833 *
1834 * @since 1.8.1
1835 *
1836 * @param int $limit The limit of items per page.
1837 * @param int $current_page The (current) page number.
1838 * @param string $report_type The report type.
1839 *
1840 * @return float
1841 */
1842 public function generate_pagination_html( $limit = 10, $current_page = 1, $report_type = 'donors-top' ) {
1843
1844 $donors = (array) $this->donors;
1845
1846 $limit = charitable_reports_is_ajax() && isset( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? intval( $_POST['limit'] ) : $limit; // phpcs:ignore
1847 $limit = false !== $limit && is_numeric( $limit ) ? $limit : charitable_reports_get_pagination_per_page();
1848
1849 $current_page = charitable_reports_is_ajax() && isset( $_POST['ppage'] ) && is_numeric( $_POST['ppage'] ) ? intval( $_POST['ppage'] ) : $current_page; // phpcs:ignore
1850 $current_page = is_numeric( $current_page ) ? $current_page : 1;
1851
1852 $pagination = array(
1853 'length' => isset( $_GET['limit'] ) ? (int) $_GET['limit'] : $limit, // phpcs:ignore
1854 'total' => 0 !== $this->donors_total ? $this->donors_total : count( $donors ),
1855 'currentPage' => isset( $_GET['ppage'] ) ? (int) $_GET['ppage'] : $current_page, // phpcs:ignore
1856 );
1857
1858 $pagination['total_pages'] = ceil( $pagination['total'] / $pagination['length'] );
1859 $pagination['offset'] = ( $pagination['currentPage'] * $pagination['length'] ) - $pagination['length'];
1860
1861 // The params for the pagination links, which might depend on report type.
1862 $arr_params = array(
1863 'page' => 'charitable-reports',
1864 'tab' => 'donors',
1865 'report-type' => isset( $_GET['report_type'] ) ? sanitize_text_field( wp_unslash( $_GET['report_type'] ) ) : $report_type, // phpcs:ignore
1866 );
1867
1868 ob_start();
1869
1870 $this->show_pagination( $pagination['total_pages'], $pagination['currentPage'], $pagination['total'], $pagination['offset'], $pagination['length'], $arr_params );
1871
1872 return ob_get_clean();
1873 }
1874
1875 /**
1876 * Get the activity icon url from a status string.
1877 *
1878 * @since 1.8.1
1879 *
1880 * @param string $icon The icon name.
1881 *
1882 * @return string
1883 */
1884 public function get_activity_icon_url( $icon = false ) {
1885
1886 if ( false === $icon || '' === $icon ) {
1887 return false;
1888 }
1889
1890 return charitable()->get_path( 'directory', false ) . 'assets/images/icons/activities/' . $icon . '.png';
1891 }
1892
1893 /**
1894 * Get the top donors for the dedicated donor report.
1895 *
1896 * @since 1.8.1
1897 *
1898 * @param int $limit The limit of donors to get.
1899 * @param int $offset The offset of donors to get.
1900 *
1901 * @return string
1902 */
1903 public function get_top_donors( $limit = false, $offset = false ) { // phpcs:ignore
1904 if ( empty( $this->donors ) ) {
1905 return;
1906 }
1907
1908 return $this->donors;
1909 }
1910
1911 /**
1912 * Get the top donors for the dedicated donor report.
1913 *
1914 * @since 1.8.1
1915 *
1916 * @param int $limit The limit of donors to get.
1917 *
1918 * @return string
1919 */
1920 public function get_recurring_donors( $limit = false ) { // phpcs:ignore
1921 if ( empty( $this->donors ) ) {
1922 return;
1923 }
1924
1925 return $this->donors;
1926 }
1927
1928 /**
1929 * Get the first time donors for the dedicated donor report.
1930 *
1931 * @since 1.8.1
1932 *
1933 * @param int $limit The limit of donors to get.
1934 *
1935 * @return string
1936 */
1937 public function get_first_time_donors( $limit = false ) { // phpcs:ignore
1938 if ( empty( $this->donors ) ) {
1939 return;
1940 }
1941
1942 return $this->donors;
1943 }
1944
1945 /**
1946 * Get the donors for the dedicated donor report.
1947 *
1948 * @since 1.8.1
1949 *
1950 * @param string $report_type The report type.
1951 * @param array $args The array of args.
1952 *
1953 * @return array|bool
1954 */
1955 public function get_donor_report_by_type( $report_type = false, $args = array() ) {
1956
1957 $defaults = array(
1958 'limit' => false,
1959 'offset' => false,
1960 'ppage' => false,
1961 );
1962
1963 $args = wp_parse_args( $args, $defaults );
1964
1965 // Extract individual variables from args array.
1966 $limit = $args['limit'] ?? false;
1967 $offset = $args['offset'] ?? false;
1968 $ppage = $args['ppage'] ?? false;
1969
1970 if ( ! $offset && $ppage && $limit ) {
1971 $offset = ( $ppage - 1 ) * $limit;
1972 }
1973
1974 switch ( $report_type ) {
1975 case 'donors-top':
1976 return ( $this->get_top_donors( $limit, $offset ) );
1977 break; // phpcs:ignore
1978 case 'donors-recurring':
1979 return ( $this->get_recurring_donors( $limit, $offset ) );
1980 break; // phpcs:ignore
1981 case 'donors-first-time':
1982 return ( $this->get_first_time_donors( $limit, $offset ) );
1983 break; // phpcs:ignore
1984
1985 default:
1986 return false;
1987 break; // phpcs:ignore
1988 }
1989 }
1990
1991 /**
1992 * Return the HTML of the filter select dropdown.
1993 *
1994 * @since 1.8.1
1995 *
1996 * @param string $selected_value The selected value.
1997 *
1998 * @return string
1999 */
2000 public function get_donor_report_type_dropdown( $selected_value = false ) {
2001
2002 $options_values = apply_filters(
2003 'charitable_dashboard_donor_report_type_filter_values',
2004 array(
2005 'donors-top' => __( 'Top Donors', 'charitable' ),
2006 'donors-first-time' => __( 'First Time Donors', 'charitable' ),
2007 'donors-recurring' => __( 'Recurring Donors', 'charitable' ),
2008 )
2009 );
2010
2011 ob_start();
2012
2013 ?>
2014
2015 <select name="action" id="report-donor-type-filter">
2016 <?php foreach ( $options_values as $value => $label ) : ?>
2017 <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_value, $value ); ?>><?php echo esc_html( $label ); ?></option>
2018 <?php endforeach; ?>
2019 </select>
2020
2021 <?php
2022
2023 return ob_get_clean();
2024 }
2025
2026 /**
2027 * Get the top donors for the overview report.
2028 *
2029 * @since 1.8.1
2030 *
2031 * @param int $limit The limit of donors to get.
2032 *
2033 * @return string
2034 */
2035 public function get_top_donors_overview( $limit = false ) { // phpcs:ignore
2036 if ( empty( $this->donations ) ) {
2037 return;
2038 }
2039
2040 $donations_by_donors = (array) $this->donations;
2041 $unique_donors = array();
2042
2043 // sort multi-dimensional array by 'amount'.
2044 usort(
2045 $donations_by_donors,
2046 function ( $a, $b ) {
2047 return $b->total_amount - $a->total_amount;
2048 }
2049 );
2050
2051 foreach ( $donations_by_donors as $index => $donation ) {
2052
2053 if ( empty( $donation->donor_id ) || 0 === $donation->donor_id ) {
2054 continue;
2055 }
2056
2057 $donor = charitable_get_table( 'donors' )->get_personal_data_by_donor_id( $donation->donor_id );
2058
2059 if ( empty( $donor ) ) {
2060 continue;
2061 }
2062
2063 $donor = $donor[0];
2064
2065 // combine first and last name from $donor into $donor_name, if either exist.
2066 $donor_name = ( ! empty( $donor->first_name ) || ! empty( $donor->last_name ) ? $donor->first_name . ' ' . $donor->last_name : 'Unknown' );
2067 $donor_email = ( ! empty( $donor->email ) ) ? $donor->email : '';
2068
2069 $donations_by_donors[ $index ]->donor_name = trim( $donor_name );
2070 $donations_by_donors[ $index ]->donor_email = $donor_email;
2071
2072 $donations_by_donors[ $index ]->donor_avatar_url = isset( $donation->user_id ) && $donation->user_id !== 0 ? get_avatar_url( $donation->user_id, 32 ) : 0;
2073 $donations_by_donors[ $index ]->donation_type = charitable_get_donation( $donation->donation_id )->get( 'donation_type' );
2074 $donations_by_donors[ $index ]->donation_parent_id = $donation->donation_parent_id;
2075
2076 if ( ! isset( $unique_donors[ $donation->donor_id ] ) ) {
2077 $unique_donors[ $donation->donor_id ]['amount'] = $donation->total_amount;
2078 $unique_donors[ $donation->donor_id ]['count'] = 1;
2079 } else {
2080 $unique_donors[ $donation->donor_id ]['amount'] += $donation->total_amount;
2081 $unique_donors[ $donation->donor_id ]['count'] = $unique_donors[ $donation->donor_id ]['count'] + 1;
2082 }
2083 }
2084
2085 // go back and add the total donation amount to the $donations_by_donors array.
2086 foreach ( $donations_by_donors as $index => $donation ) {
2087
2088 if ( ! empty( $unique_donors[ $donation->donor_id ] ) ) {
2089 $donations_by_donors[ $index ]->total_donation_amount = $unique_donors[ $donation->donor_id ]['amount'];
2090 $donations_by_donors[ $index ]->total_donation_count = $unique_donors[ $donation->donor_id ]['count'];
2091 }
2092 }
2093
2094 // sort multi-dimensional array by 'total_donation_amount'.
2095 // @since 1.8.8.6 - Fixed undefined property warning by checking if property exists before accessing.
2096 usort(
2097 $donations_by_donors,
2098 function ( $a, $b ) {
2099 // @since 1.8.8.6 - Check if total_donation_amount exists, default to 0 if not set.
2100 $a_amount = isset( $a->total_donation_amount ) ? $a->total_donation_amount : 0;
2101 $b_amount = isset( $b->total_donation_amount ) ? $b->total_donation_amount : 0;
2102 return $b_amount - $a_amount;
2103 }
2104 );
2105
2106 return $donations_by_donors;
2107 }
2108
2109 /**
2110 * Get the name of the cache value (or key) for the report we use for the database.
2111 *
2112 * @since 1.8.1
2113 *
2114 * @param string $report_type The report type.
2115 * @param string $start_date The start date.
2116 * @param string $end_date The end date.
2117 * @param string $post_status The post status.
2118 * @param string $campaign_id The campaign ID.
2119 * @param string $category_id The category ID.
2120 *
2121 * @return string
2122 */
2123 public function get_cache_key( $report_type = 'overview', $start_date = false, $end_date = false, $post_status = false, $campaign_id = false, $category_id = false ) {
2124
2125 $cache_key = sanitize_title( $start_date . $end_date . str_replace( 'charitable', '', $post_status ) . $campaign_id . $category_id );
2126
2127 // remove all dashes from the cache key.
2128 $cache_key = 'wpch-report-' . $report_type . '-' . str_replace( '-', '', $cache_key );
2129
2130 return $cache_key;
2131 }
2132
2133 /**
2134 * Get the cached report.
2135 *
2136 * @since 1.8.1
2137 *
2138 * @param string $report_type The report type.
2139 * @param string $start_date The start date.
2140 * @param string $end_date The end date.
2141 * @param string $post_status The post status.
2142 * @param string $campaign_id The campaign ID.
2143 * @param string $category_id The category ID.
2144 *
2145 * @return array|bool
2146 */
2147 public function get_cached_report( $report_type = 'overview', $start_date = false, $end_date = false, $post_status = false, $campaign_id = false, $category_id = false ) {
2148
2149 if ( ! $this->maybe_cache_report( true ) ) {
2150 return false;
2151 }
2152
2153 $cache_key = $this->get_cache_key( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id );
2154
2155 $data = get_transient( $cache_key );
2156
2157 if ( false === $data ) {
2158 return false;
2159 }
2160
2161 return $data;
2162 }
2163
2164 /**
2165 * Set the cached report.
2166 *
2167 * @since 1.8.1
2168 *
2169 * @param string $report_type The report type.
2170 * @param string $start_date The start date.
2171 * @param string $end_date The end date.
2172 * @param string $post_status The post status.
2173 * @param string $campaign_id The campaign ID.
2174 * @param string $category_id The category ID.
2175 * @param array $data The data to cache.
2176 *
2177 * @return bool
2178 */
2179 public function set_cached_report( $report_type = 'overview', $start_date = false, $end_date = false, $post_status = false, $campaign_id = false, $category_id = false, $data = array() ) {
2180
2181 if ( ! $this->maybe_cache_report( true ) ) {
2182 return false;
2183 }
2184
2185 $cache_key = $this->get_cache_key( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id );
2186
2187 $was_set = set_transient( $cache_key, $data, MINUTE_IN_SECONDS );
2188
2189 return $was_set;
2190 }
2191
2192 /**
2193 * Check if the report should be cached.
2194 *
2195 * @since 1.8.1
2196 *
2197 * @param bool $force_cache Whether to force the cache.
2198 *
2199 * @return bool
2200 */
2201 public function maybe_cache_report( $force_cache = false ) {
2202
2203 // Check the globals to see if caching is enabled specifically for reports.
2204 if ( $force_cache ) {
2205 return true;
2206 }
2207 if ( charitable_is_debug() ) {
2208 return false;
2209 }
2210 if ( defined( 'CHARITABLE_REPORTS_NO_CACHE' ) && CHARITABLE_REPORTS_NO_CACHE ) { // phpcs:ignore
2211 return false;
2212 }
2213
2214 return true;
2215 }
2216
2217 /**
2218 * This accepts a report type and date and attemps to create a report that has multiple data points
2219 *
2220 * @param string $report_type The type of report to create.
2221 * @param array $args The array of args.
2222 *
2223 * @since 1.8.1
2224 *
2225 * @return array
2226 */
2227 public function get_data( $report_type = 'overview', $args = array() ) {
2228
2229 global $wpdb;
2230
2231 $defaults = array(
2232 'report_type' => 'overview',
2233 'start_date' => false,
2234 'end_date' => false,
2235 'post_status' => 'charitable-completed',
2236 'campaign_id' => false,
2237 'category_id' => false,
2238 );
2239
2240 $args = wp_parse_args( $args, $defaults );
2241
2242 // Extract individual variables from args array.
2243 $report_type = $args['report_type'] ?? 'overview';
2244 $start_date = $args['start_date'] ?? false;
2245 $end_date = $args['end_date'] ?? false;
2246 $post_status = $args['post_status'] ?? 'charitable-completed';
2247 $campaign_id = $args['campaign_id'] ?? false;
2248 $category_id = $args['category_id'] ?? false;
2249
2250 // Check the post_status and set it to the default if it's not a valid status.
2251 if ( ! in_array( $post_status, array( 'charitable-completed', 'charitable-pending', 'charitable-failed', 'charitable-cancelled', 'charitable-refunded' ), true ) ) {
2252 $post_status = 'charitable-completed';
2253 }
2254
2255 $this->donations = ! charitable_is_pro() && $report_type !== 'overview' ? $this->get_data_sample( $report_type ) : false;
2256
2257 // Determine if this particular report has been cached recently.
2258 $this->donations = false; // $this->get_cached_report( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id );
2259
2260 if ( false === $this->donations ) {
2261
2262 // No cache? Let's run the data.
2263
2264 $where_sql = array();
2265 $where_sql[] = 'WHERE 1=1';
2266 $where_sql[] = ( intval( $campaign_id ) > 0 ) ? 'cd.campaign_id = ' . intval( $campaign_id ) : false;
2267 $where_sql[] = 'p.post_type = "%s"';
2268 $where_sql[] = 'p.post_status = "' . $post_status . '"';
2269 $where_sql[] = ( $start_date ) ? "p.post_date >= '" . $start_date . " 00:00:00'" : false;
2270 $where_sql[] = ( $end_date ) ? "p.post_date <= '" . $end_date . " 23:59:59'" : false;
2271 $where_sql[] = ( intval( $category_id ) > 0 ) ? 'tr.term_taxonomy_id = ' . intval( $category_id ) : false;
2272 $where_sql[] = 'pm1.meta_key = "donation_gateway"';
2273 // remove all empty values from the array.
2274 $where_sql = array_filter( $where_sql );
2275 $where_args = implode( ' AND ', $where_sql );
2276
2277 $left_join = array();
2278 $left_join[] = $wpdb->prefix . 'charitable_campaign_donations cd ON p.ID = cd.donation_id';
2279 $left_join[] = $wpdb->prefix . 'charitable_donors cdonors ON cd.donor_id = cdonors.donor_id';
2280 $left_join[] = $wpdb->prefix . 'postmeta pm1 ON p.ID = pm1.post_id';
2281 $left_join[] = ( intval( $category_id ) > 0 ) ? $wpdb->prefix . 'term_relationships tr ON tr.object_id = campaign_id' : false;
2282 // remove all empty values from the array.
2283 $left_join = array_filter( $left_join );
2284 $left_join_args = 'LEFT JOIN ' . implode( ' LEFT JOIN ', $left_join );
2285
2286 $sql = "SELECT SUM(cd.amount) AS total_amount,
2287 cd.donation_id AS donation_id,
2288 cd.campaign_id AS campaign_id,
2289 cd.donor_id AS donor_id,
2290 cd.amount AS amount,
2291 pm1.meta_value AS payment_gateway,
2292 p.post_date AS post_date,
2293 p.post_date_gmt AS post_date_gmt,
2294 p.post_status AS donation_status,
2295 p.post_parent AS donation_parent_id,
2296 cdonors.first_name AS first_name,
2297 cdonors.last_name AS last_name,
2298 cdonors.email AS email
2299 FROM $wpdb->posts p
2300 " . $left_join_args . '
2301 ' . $where_args . '
2302 GROUP BY p.ID';
2303
2304 // get all donations (posts with a post type of donation) between a start date and an end date.
2305 $this->donations = $wpdb->get_results( // phpcs:ignore
2306 $wpdb->prepare(
2307 $sql, // phpcs:ignore
2308 'donation'
2309 )
2310 );
2311
2312 // Save this to the cache.
2313 // $this->set_cached_report( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id, $this->donations );.
2314
2315 }
2316
2317 $this->init_axis_with_donations( $this->donations, $start_date, $end_date );
2318
2319 return $this->donations;
2320 }
2321
2322 /**
2323 * This accepts a report type and date and attemps to create a report specific to a donor type report.
2324 *
2325 * @since 1.8.1
2326 *
2327 * @param string $report_type The type of report to create.
2328 * @param string $start_date The start date.
2329 * @param string $end_date The end date.
2330 * @param string $campaign_id The campaign ID.
2331 * @param string $category_id The category ID.
2332 *
2333 * @return array
2334 */
2335 public function get_donors_data( $report_type = 'donors-top', $start_date = false, $end_date = false, $campaign_id = false, $category_id = false ) { // phpcs:ignore
2336
2337 global $wpdb;
2338
2339 // Determine if this particular report gets sample data, or if not if it's has been cached recently.
2340 $this->donors = ! charitable_is_pro() ? $this->get_data_sample( $report_type ) : false; // $this->get_cached_report( $report_type, $start_date, $end_date, false, $campaign_id, $category_id );
2341
2342 if ( false === $this->donors ) {
2343
2344 // No cache? Let's run the data.
2345
2346 $having_sql = '';
2347 $order_by = '';
2348
2349 switch ( $report_type ) {
2350 case 'donors-recurring':
2351 $having_sql = 'HAVING total_count_donations > 1';
2352 $order_by = 'ORDER BY %s DESC';
2353 $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_recurring_sql_limit', 100 );
2354
2355 $sql = "SELECT SUM( ccd.amount ) AS total_amount,
2356 AVG( ccd.amount ) AS average,
2357 COUNT( ccd.donation_id ) AS total_count_donations,
2358 COUNT( DISTINCT( ccd.campaign_id ) ) AS total_count_campaigns,
2359 last_donation.post_date AS last_donation_date,
2360 last_campaign.post_title AS last_campaign_title,
2361 last_campaign.ID AS last_campaign_id,
2362 cd.donor_id, cd.user_id, cd.email, cd.first_name, cd.last_name, cd.date_joined, ccd.donation_id, ccd.campaign_id, ccd.amount
2363
2364 FROM {$wpdb->prefix}charitable_donors cd
2365
2366 JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id
2367 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ORDER BY post_date DESC ) last_donation ON ccd.donation_id = last_donation.ID
2368 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ORDER BY post_date DESC ) last_campaign ON ccd.campaign_id = last_campaign.ID
2369
2370 GROUP BY cd.donor_id
2371
2372 {$having_sql}
2373
2374 {$order_by}
2375
2376 {$limit}
2377
2378 ";
2379
2380 $this->donors = $wpdb->get_results( // phpcs:ignore
2381 $wpdb->prepare(
2382 $sql, // phpcs:ignore
2383 'total_amount'
2384 )
2385 );
2386
2387 break;
2388
2389 case 'donors-first-time':
2390 $having_sql = 'HAVING total_count_donations = 1';
2391 $order_by = 'ORDER BY %s DESC'; // not currently being used.
2392 $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_first_time_sql_limit', 100 );
2393
2394 $sql = "SELECT ccd.amount AS total_amount,
2395 COUNT( ccd.donation_id ) AS total_count_donations,
2396 last_donation.post_date AS last_donation_date,
2397 last_campaign.post_title AS last_campaign_title,
2398 last_campaign.ID AS last_campaign_id,
2399 cd.donor_id, cd.user_id, cd.email, cd.first_name, cd.last_name, cd.date_joined, ccd.donation_id, ccd.campaign_id, ccd.amount
2400
2401 FROM {$wpdb->prefix}charitable_donors cd
2402
2403 JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id
2404 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ORDER BY post_date DESC ) last_donation ON ccd.donation_id = last_donation.ID
2405 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ORDER BY post_date DESC ) last_campaign ON ccd.campaign_id = last_campaign.ID
2406
2407 GROUP BY cd.donor_id
2408
2409 {$having_sql}
2410
2411 ORDER BY last_donation_date DESC
2412
2413 {$limit}
2414
2415 ";
2416
2417 $this->donors = $wpdb->get_results( // phpcs:ignore
2418 $wpdb->prepare(
2419 $sql // phpcs:ignore
2420 )
2421 );
2422
2423 break;
2424
2425 default:
2426 $order_by = 'ORDER BY %s DESC';
2427 $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_default_sql_limit', 100, $report_type );
2428
2429 $sql = "SELECT SUM( ccd.amount ) AS total_amount,
2430 AVG( ccd.amount ) AS average,
2431 COUNT( ccd.donation_id ) AS total_count_donations,
2432 COUNT( DISTINCT( ccd.campaign_id ) ) AS total_count_campaigns,
2433 last_donation.post_date AS last_donation_date,
2434 last_campaign.post_title AS last_campaign_title,
2435 last_campaign.ID AS last_campaign_id,
2436 cd.donor_id, cd.user_id, cd.email, cd.first_name, cd.last_name, cd.date_joined, ccd.donation_id, ccd.campaign_id, ccd.amount
2437
2438 FROM {$wpdb->prefix}charitable_donors cd
2439
2440 JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id
2441 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ORDER BY post_date DESC ) last_donation ON ccd.donation_id = last_donation.ID
2442 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ORDER BY post_date DESC ) last_campaign ON ccd.campaign_id = last_campaign.ID
2443
2444 GROUP BY cd.donor_id
2445
2446 {$having_sql}
2447
2448 {$order_by}
2449
2450 {$limit}
2451
2452 ";
2453
2454 $this->donors = $wpdb->get_results( // phpcs:ignore
2455 $wpdb->prepare(
2456 $sql, // phpcs:ignore
2457 'total_amount'
2458 )
2459 );
2460
2461 break;
2462 }
2463
2464 // Save this to the cache.
2465 // $this->set_cached_report( $report_type, $start_date, $end_date, false, $campaign_id, $category_id, $this->donors );.
2466
2467 return $this->donors;
2468
2469 }
2470 }
2471
2472 /**
2473 * This accepts a report type and date and attemps to create a report that has multiple data points
2474 *
2475 * @since 1.8.1
2476 * @since 1.8.3 Updated w/ limit and offset sql, etc.
2477 *
2478 * @param string $report_type The type of report to create.
2479 * @param array $args The array of args.
2480 *
2481 * @return array
2482 */
2483 public function get_donors_data_array( $report_type = 'donors-top', $args = array() ) {
2484
2485 global $wpdb;
2486
2487 $defaults = array(
2488 'report_type' => 'donors-top',
2489 'start_date' => false,
2490 'end_date' => false,
2491 'campaign_id' => false,
2492 'category_id' => false,
2493 'limit' => false,
2494 'offset' => false,
2495 );
2496
2497 $args = wp_parse_args( $args, $defaults );
2498
2499 // Extract individual variables from args array.
2500 $report_type = $args['report_type'] ?? 'donors-top';
2501 $start_date = $args['start_date'] ?? false;
2502 $end_date = $args['end_date'] ?? false;
2503 $campaign_id = $args['campaign_id'] ?? false;
2504 $category_id = $args['category_id'] ?? false;
2505 $limit = $args['limit'] ?? false;
2506 $offset = $args['offset'] ?? false;
2507 $ppage = $args['ppage'] ?? false;
2508
2509 // Determine if this particular report gets sample data, or if not if it's has been cached recently.
2510 $this->donors = ! charitable_is_pro() ? $this->get_data_sample( $report_type ) : false; // $this->get_cached_report( $report_type, $start_date, $end_date, false, $campaign_id, $category_id );
2511
2512 if ( false === $this->donors ) {
2513
2514 // No cache? Let's run the data.
2515
2516 $having_sql = '';
2517 $order_by = '';
2518 $where = '';
2519 // generate mysql LIMIT and OFFSET.
2520 if ( ! $offset && $ppage && $limit ) {
2521 $offset = ( $ppage - 1 ) * $limit;
2522 }
2523 $limit_sql = ( $limit ) ? 'LIMIT ' . $limit : '';
2524 $offset_sql = ( $offset ) ? 'OFFSET ' . $offset : '';
2525
2526 switch ( $report_type ) {
2527 case 'donors-recurring':
2528 $having_sql = 'HAVING total_count_donations > 1';
2529 $order_by = 'ORDER BY total_amount DESC';
2530
2531 $query = "SELECT SUM( ccd2.amount ) AS total_amount,
2532 AVG( ccd2.amount ) AS average,
2533 COUNT( ccd2.donation_id ) AS total_count_donations,
2534 COUNT( DISTINCT( ccd2.campaign_id ) ) AS total_count_campaigns,
2535 last_donation.post_date AS last_donation_date,
2536 last_campaign.post_title AS last_campaign_title,
2537 last_campaign.ID AS last_campaign_id,
2538 cd.donor_id, cd.user_id, cd.email, cd.first_name, cd.last_name, cd.date_joined, ccd.donation_id, ccd.campaign_id, ccd.amount, ccd.campaign_donation_id AS CAMPAIGN_DONATION_ID
2539
2540 FROM {$wpdb->prefix}charitable_donors cd
2541
2542 JOIN ( SELECT {$wpdb->prefix}charitable_campaign_donations.donor_id, {$wpdb->prefix}charitable_campaign_donations.donation_id, {$wpdb->prefix}charitable_campaign_donations.campaign_id, {$wpdb->prefix}charitable_campaign_donations.amount, {$wpdb->prefix}charitable_campaign_donations.campaign_donation_id
2543 FROM {$wpdb->prefix}charitable_campaign_donations
2544 JOIN ( SELECT MAX(campaign_donation_id) AS max_campaign_donation_id, donor_id FROM {$wpdb->prefix}charitable_campaign_donations JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}charitable_campaign_donations.donation_ID WHERE {$wpdb->prefix}posts.post_status IN ('charitable-completed') GROUP BY donor_id ) last_campaign_donation_id ON last_campaign_donation_id.max_campaign_donation_id = {$wpdb->prefix}charitable_campaign_donations.campaign_donation_id
2545 ) ccd ON ccd.donor_id = cd.donor_id
2546
2547 JOIN ( SELECT donor_id, donation_id, campaign_id, amount, campaign_donation_id FROM {$wpdb->prefix}charitable_campaign_donations
2548 JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}charitable_campaign_donations.donation_ID
2549 WHERE {$wpdb->prefix}posts.post_status IN ('charitable-completed')
2550
2551 ) ccd2 ON ccd2.donor_id = cd.donor_id
2552
2553 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_donation ON ccd.donation_id = last_donation.ID
2554 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_campaign ON ccd.campaign_id = last_campaign.ID
2555
2556 {$where}
2557
2558 GROUP BY cd.donor_id
2559
2560 {$having_sql}
2561
2562 {$order_by}
2563
2564 ";
2565
2566 $this->donors = $wpdb->get_results( // phpcs:ignore
2567 $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore
2568 );
2569
2570 break;
2571
2572 case 'donors-first-time':
2573 $having_sql = 'HAVING total_count_donations = 1';
2574 $order_by = 'ORDER BY last_donation_date ASC';
2575
2576 $query = "SELECT ccd.amount AS total_amount,
2577 COUNT( ccd.donation_id ) AS total_count_donations,
2578 last_donation.post_date AS last_donation_date,
2579 last_campaign.post_title AS last_campaign_title,
2580 last_campaign.ID AS last_campaign_id,
2581 cd.donor_id, cd.user_id, cd.email, cd.first_name, cd.last_name, cd.date_joined, ccd.donation_id, ccd.campaign_id, ccd.amount
2582
2583 FROM {$wpdb->prefix}charitable_donors cd
2584
2585 JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id
2586 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ORDER BY post_date DESC ) last_donation ON ccd.donation_id = last_donation.ID
2587 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ORDER BY post_date DESC ) last_campaign ON ccd.campaign_id = last_campaign.ID
2588
2589 {$where}
2590
2591 GROUP BY cd.donor_id
2592
2593 {$having_sql}
2594
2595 {$order_by}
2596
2597 ";
2598
2599 $this->donors = $wpdb->get_results( // phpcs:ignore
2600 $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore
2601 );
2602
2603 break;
2604
2605 default:
2606 $order_by = 'ORDER BY total_amount DESC';
2607
2608 $query = "SELECT SUM( ccd2.amount ) AS total_amount,
2609 AVG( ccd2.amount ) AS average,
2610 COUNT( ccd2.donation_id ) AS total_count_donations,
2611 COUNT( DISTINCT( ccd2.campaign_id ) ) AS total_count_campaigns,
2612 last_donation.post_date AS last_donation_date,
2613 last_campaign.post_title AS last_campaign_title,
2614 last_campaign.ID AS last_campaign_id,
2615 cd.donor_id, cd.user_id, cd.email, cd.first_name, cd.last_name, cd.date_joined, ccd.donation_id, ccd.campaign_id, ccd.amount, ccd.campaign_donation_id AS CAMPAIGN_DONATION_ID
2616
2617 FROM {$wpdb->prefix}charitable_donors cd
2618
2619 JOIN ( SELECT {$wpdb->prefix}charitable_campaign_donations.donor_id, {$wpdb->prefix}charitable_campaign_donations.donation_id, {$wpdb->prefix}charitable_campaign_donations.campaign_id, {$wpdb->prefix}charitable_campaign_donations.amount, {$wpdb->prefix}charitable_campaign_donations.campaign_donation_id
2620 FROM {$wpdb->prefix}charitable_campaign_donations
2621 JOIN ( SELECT MAX(campaign_donation_id) AS max_campaign_donation_id, donor_id FROM {$wpdb->prefix}charitable_campaign_donations JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}charitable_campaign_donations.donation_ID WHERE {$wpdb->prefix}posts.post_status IN ('charitable-completed') GROUP BY donor_id ) last_campaign_donation_id ON last_campaign_donation_id.max_campaign_donation_id = {$wpdb->prefix}charitable_campaign_donations.campaign_donation_id
2622 ) ccd ON ccd.donor_id = cd.donor_id
2623
2624 JOIN ( SELECT donor_id, donation_id, campaign_id, amount, campaign_donation_id FROM {$wpdb->prefix}charitable_campaign_donations
2625 JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}charitable_campaign_donations.donation_ID
2626 WHERE {$wpdb->prefix}posts.post_status IN ('charitable-completed')
2627
2628 ) ccd2 ON ccd2.donor_id = cd.donor_id
2629
2630 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_donation ON ccd.donation_id = last_donation.ID
2631 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_campaign ON ccd.campaign_id = last_campaign.ID
2632
2633 {$where}
2634
2635 GROUP BY cd.donor_id
2636
2637 {$having_sql}
2638
2639 {$order_by}";
2640
2641 $this->donors = $wpdb->get_results( // phpcs:ignore
2642 $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore
2643 );
2644
2645 break;
2646 }
2647
2648 $total_query = 'SELECT COUNT(1) FROM (' . $query . ') AS combined_table';
2649 $this->donors_total = $wpdb->get_var( $total_query ); // phpcs:ignore
2650
2651 // Save this to the cache.
2652 // $this->set_cached_report( $report_type, $start_date, $end_date, false, $campaign_id, $category_id, $this->donors );.
2653
2654 return $this->donors;
2655
2656 }
2657 }
2658
2659 /**
2660 * This accepts donations and attempts to create data for the headline chart with donation data.
2661 *
2662 * @param array $donations An array of donations.
2663 * @param string $start_date The start date.
2664 * @param string $end_date The end date.
2665 *
2666 * @since 1.8.1
2667 */
2668 public function init_axis_with_donations( $donations = array(), $start_date = false, $end_date = false ) {
2669
2670 if ( false === $donations ) {
2671 return;
2672 }
2673
2674 $date_data = array();
2675 $donation_data = array();
2676
2677 foreach ( $donations as $donation ) {
2678
2679 $donation_day = gmdate( 'M d', strtotime( $donation->post_date ) );
2680
2681 if ( ! in_array( $donation_day, $date_data, true ) ) {
2682 $date_data[] = $donation_day;
2683 }
2684
2685 if ( isset( $donation_data[ $donation_day ]['amount'] ) ) {
2686 $donation_data[ $donation_day ]['amount'] += $donation->total_amount;
2687 } else {
2688 $donation_data[ $donation_day ]['amount'] = $donation->total_amount;
2689 }
2690
2691 }
2692
2693 $this->set_date_axis( $date_data, $start_date, $end_date );
2694 $this->set_donation_axis( $donation_data, $start_date, $end_date );
2695 }
2696
2697 /**
2698 * This generates the donation axis for the headline chart.
2699 *
2700 * @param array $donation_data An array of donations.
2701 * @param string $start_date The start date.
2702 * @param string $end_date The end date.
2703 *
2704 * @since 1.8.1
2705 */
2706 public function set_donation_axis( $donation_data, $start_date, $end_date ) {
2707
2708 $this->donation_axis = array();
2709
2710 $days = $this->get_days_between_dates( $start_date, $end_date );
2711
2712 if ( ! $days || empty( $days ) ) {
2713 return;
2714 }
2715
2716 foreach ( $days as $day ) {
2717 $this->donation_axis[ $day ] = ! empty( $donation_data[ $day ]['amount'] ) ? $donation_data[ $day ]['amount'] : 0;
2718 }
2719
2720 // Remove the keys from the array but keep the order of the values.
2721 $this->donation_axis = array_values( $this->donation_axis );
2722
2723 // if the days is only one day, add a day before and after that date so the chart has at least 3 days for display.
2724 if ( count( $days ) === 1 ) {
2725 $this->donation_axis = array_merge( array( 0 ), $this->donation_axis, array( 0 ) );
2726 }
2727
2728 return $this->donation_axis;
2729 }
2730
2731 /**
2732 * This generates the date axis for the headline chart.
2733 *
2734 * @param array $date_data An array of dates.
2735 * @param string $start_date The start date.
2736 * @param string $end_date The end date.
2737 *
2738 * @since 1.8.1
2739 */
2740 public function set_date_axis( $date_data, $start_date, $end_date ) {
2741
2742 $days = $this->get_days_between_dates( $start_date, $end_date, false );
2743
2744 // if the days is only one day, then add a day before and after that date so the chart has at least 3 days for display.
2745 if ( count( $days ) === 1 ) {
2746 $days = array_merge( array( gmdate( 'M d', strtotime( $start_date . ' -1 day' ) ) ), $days, array( gmdate( 'M d', strtotime( $end_date . ' +1 day' ) ) ) );
2747 }
2748
2749 $this->date_axis = $days;
2750
2751 return $days;
2752 }
2753
2754 /**
2755 * Util function that gets dates between two given dates.
2756 *
2757 * @param string $start_date The start date.
2758 * @param string $end_date The end date.
2759 * @param string $array_key_format The format of the array key.
2760 *
2761 * @since 1.8.1
2762 *
2763 * @return mixed
2764 */
2765 public function get_days_between_dates( $start_date, $end_date, $array_key_format = 'Y-m-d' ) { // phpcs:ignore
2766 $days = array();
2767
2768 $current_date = new DateTime( $start_date );
2769 $end_date = new DateTime( $end_date );
2770
2771 while ( $current_date <= $end_date ) {
2772 if ( $array_key_format ) :
2773 $days[ $current_date->format( $array_key_format ) ] = $current_date->format( 'M d' );
2774 else :
2775 $days[] = $current_date->format( 'M d' );
2776 endif;
2777
2778 $current_date->modify( '+1 day' );
2779 }
2780
2781 return $days;
2782 }
2783
2784 /**
2785 * This accepts a report type and date and attemps to create a refund "report".
2786 *
2787 * @since 1.8.1
2788 * @version 1.8.8.6
2789 *
2790 * @param string $campaign_id The campaign ID.
2791 * @param string $limit The limit of refunds to get.
2792 *
2793 * @return array
2794 */
2795 public function get_refunds( $campaign_id = false, $limit = false ) { // phpcs:ignore
2796
2797 global $wpdb;
2798
2799 $where_sql = array();
2800 $prepare_args = array();
2801
2802 $where_sql[] = 'p.post_status = %s';
2803 $prepare_args[] = 'charitable-refunded';
2804
2805 // add to $where_sql depending on the values of $start_date and $end_date.
2806 if ( $this->start_date ) {
2807 $where_sql[] = 'p.post_date >= %s';
2808 $prepare_args[] = $this->start_date . ' 00:00:00';
2809 }
2810 if ( $this->end_date ) {
2811 $where_sql[] = 'p.post_date <= %s';
2812 $prepare_args[] = $this->end_date . ' 23:59:59';
2813 }
2814
2815 $where_sql[] = 'pm1.meta_key = %s';
2816 $prepare_args[] = 'donation_gateway';
2817
2818 $where_clause = implode( ' AND ', $where_sql );
2819
2820 // Build prepare args with post_type first, then where clause args.
2821 $all_prepare_args = array_merge( array( 'donation' ), $prepare_args );
2822
2823 // get all donations (posts with a post type of donation) between a start date and an end date.
2824 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
2825 // $where_clause is built with placeholders (%s) that are properly processed by $wpdb->prepare().
2826 // The interpolation is safe because all values in $where_clause are placeholders, not raw values.
2827 $refunded_donations = $wpdb->get_results(
2828 $wpdb->prepare(
2829 "SELECT SUM(cd.amount) AS total_amount,
2830 COUNT(p.ID) AS total_number_of_refunds,
2831 cd.donation_id AS donation_id,
2832 cd.campaign_id AS campaign_id,
2833 cd.donor_id AS donor_id,
2834 cd.amount AS amount,
2835 pm1.meta_value AS payment_gateway,
2836 p.post_date AS post_date,
2837 p.post_date_gmt AS post_date_gmt
2838 FROM {$wpdb->posts} p
2839 LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id
2840 LEFT JOIN {$wpdb->prefix}postmeta pm1 ON p.ID = pm1.post_id
2841 WHERE p.post_type = %s AND {$where_clause}
2842 GROUP BY p.ID",
2843 ...$all_prepare_args
2844 )
2845 );
2846 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
2847
2848 $refunds_by_day = array();
2849 $refunded_donations_total_amount = 0;
2850 $refunded_donations_total_count = 0;
2851
2852 $days = $this->get_days_between_dates( $this->start_date, $this->end_date );
2853
2854 /**
2855 * Bucket the refunds by day once, up front, then read from that.
2856 *
2857 * The day loop below used to iterate the day LABEL ('M d') rather than the
2858 * 'Y-m-d' key, so strtotime() stamped every bucket with the CURRENT year. The
2859 * Refunds column and Net in the breakdown table therefore read 0 for any range
2860 * outside the current year, and for a multi-year range refunds from different
2861 * years merged into a single month-day bucket. (The totals were already
2862 * computed up front, so the Refunds card itself was correct.)
2863 *
2864 * Buckets on post_date, matching the WHERE clause above and the donation series
2865 * in get_donations_by_day(). Bucketing on post_date_gmt while filtering on
2866 * post_date could put a late-evening refund in a day outside the range on a
2867 * non-UTC site.
2868 *
2869 * The GROUP BY p.ID in the query already guarantees one row per donation, so no
2870 * per-day dedup is needed. The previous "$donation_id > 0" guard also silently
2871 * dropped a refunded donation that has no campaign_donations row from the totals;
2872 * such a refund is now counted, with a zero amount, so it stays visible.
2873 */
2874 $refunds_by_date = array();
2875
2876 if ( ! empty( $refunded_donations ) ) {
2877 foreach ( $refunded_donations as $refunded_donation ) {
2878 $refund_day = gmdate( 'Y-m-d', strtotime( $refunded_donation->post_date ) );
2879
2880 if ( ! isset( $refunds_by_date[ $refund_day ] ) ) {
2881 $refunds_by_date[ $refund_day ] = array(
2882 'amount' => 0,
2883 'count' => 0,
2884 );
2885 }
2886
2887 $refunds_by_date[ $refund_day ]['amount'] += floatval( $refunded_donation->amount );
2888 ++$refunds_by_date[ $refund_day ]['count'];
2889
2890 $refunded_donations_total_amount += floatval( $refunded_donation->amount );
2891 ++$refunded_donations_total_count;
2892 }
2893 }
2894
2895 foreach ( $days as $day_unformatted => $day_label ) {
2896 $donation_day = gmdate( 'Y-m-d', strtotime( $day_unformatted ) );
2897
2898 $refunds_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day_unformatted ) );
2899 $refunds_by_day[ $donation_day ]['amount'] = isset( $refunds_by_date[ $donation_day ] ) ? $refunds_by_date[ $donation_day ]['amount'] : 0;
2900 $refunds_by_day[ $donation_day ]['donors'] = isset( $refunds_by_date[ $donation_day ] ) ? $refunds_by_date[ $donation_day ]['count'] : 0;
2901 }
2902
2903 return array(
2904 'donations' => $refunded_donations,
2905 'total_amount' => $refunded_donations_total_amount,
2906 'total_count' => $refunded_donations_total_count,
2907 'refunds_by_day' => $refunds_by_day,
2908 );
2909 }
2910
2911
2912 /**
2913 * This accepts a report type and date and attemps to create a report that has multiple data points.
2914 *
2915 * @param string $start_date The start date.
2916 * @param string $end_date The end date.
2917 *
2918 * @since 1.8.1
2919 */
2920 public function get_donation_breakdown_data( $start_date = false, $end_date = false ) { // phpcs:ignore
2921
2922 global $wpdb;
2923
2924 // Get all posts with a custom post type of 'donation' that were created on and including between $start_data and $end_date.
2925 $donations = $wpdb->get_results( // phpcs:ignore
2926 $wpdb->prepare(
2927 "SELECT SUM(cd.amount) AS total_amount, p.ID FROM $wpdb->posts p
2928 LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id
2929 WHERE p.post_type = 'donation' AND p.post_status = 'charitable-completed'
2930 GROUP BY p.ID"
2931 ) // phpcs:ignore
2932 );
2933
2934 // Get all posts with a custom post type of 'donation' that were created on and including between $start_data and $end_date - group them by day.
2935 $donations_by_day = $wpdb->get_results( // phpcs:ignore
2936 $wpdb->prepare(
2937 "SELECT SUM(cd.amount) AS total_amount, COUNT(p.ID) AS total_donors, DATE(p.post_date) AS post_date FROM $wpdb->posts p
2938 LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id
2939 WHERE p.post_type = 'donation' AND p.post_status = 'charitable-completed'
2940 GROUP BY DATE(p.post_date)"
2941 ) // phpcs:ignore
2942 );
2943 }
2944
2945
2946 /**
2947 * This gathers info from the database with a focus on donors.
2948 *
2949 * @param string $slug Slug.
2950 * @param string $start_date The start date.
2951 * @param string $end_date The end date.
2952 * @param string $return_as The return type. Object or array. Default is object.
2953 *
2954 * @since 1.8.1
2955 */
2956 public function get_donors_from_db( $slug = 'compare_from', $start_date = false, $end_date = false, $return_as = 'object' ) {
2957
2958 // what we need:
2959 //
2960 // Total Donation count between start_date and end_date.
2961 // Total Donation amount between start_date and end_date.
2962
2963 global $wpdb;
2964
2965 $return_as = ( 'array' === $return_as ) ? 'ARRAY_A' : 'OBJECT_K';
2966
2967 return ( $wpdb->get_results( // phpcs:ignore
2968 $wpdb->prepare(
2969 "SELECT cd.donor_id AS donor_id, COUNT(ccd.donation_id) as %s, SUM(ccd.amount) AS %s,
2970 cd.user_id AS user_id,
2971 cd.email AS donor_email,
2972 cd.first_name AS donor_first_name,
2973 cd.last_name AS donor_last_name
2974 FROM {$wpdb->prefix}charitable_donors cd
2975 LEFT JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON ccd.donor_id = cd.donor_id
2976 LEFT JOIN {$wpdb->prefix}posts p ON p.ID = ccd.donation_id
2977 WHERE p.post_date >= %s AND p.post_date <= %s AND p.post_status = 'charitable-completed'
2978 GROUP BY cd.donor_id",
2979 'total_donation_count_' . $slug,
2980 'total_donation_amount_' . $slug,
2981 $start_date,
2982 $end_date
2983 ),
2984 $return_as
2985 ) );
2986 }
2987
2988 /**
2989 * Generates HTML for almost the entire activities area.
2990 *
2991 * @since 1.8.1
2992 *
2993 * @param string $report_type The report type.
2994 * @param array $advanced_report_data The report data.
2995 * @param array $report_args The report args.
2996 * @param bool $show_actions Whether to show actions.
2997 *
2998 * @return string
2999 */
3000 public function generate_advanced_breakdown_report_html( $report_type = 'lybunt', $advanced_report_data = array(), $report_args = array(), $show_actions = false ) {
3001
3002 $table_meta = (array) $this->get_advanced_report_table_meta( $report_type, $report_args, $show_actions );
3003
3004 // Allow certain HTML tags in the column titles.
3005 $title_whitelist_tags = array(
3006
3007 'br' => array(),
3008
3009 );
3010
3011 ob_start();
3012
3013 ?>
3014
3015 <div class="charitable-headline-reports">
3016
3017
3018 <?php
3019 if ( ! charitable_is_pro() ) :
3020 ?>
3021 <div class="restricted-access-overlay"></div><?php endif; ?>
3022
3023 <?php echo $this->generate_advanced_cards( $advanced_report_data, $report_type, $report_args ); // phpcs:ignore ?>
3024
3025 </div>
3026
3027 <div class="tablenav charitable-section no-bottom">
3028
3029 <div class="alignleft actions">
3030
3031 <h2><?php echo esc_html( $table_meta['title'] ); ?></h2>
3032
3033 </div>
3034
3035 <div class="alignright">
3036
3037 <?php if ( ! charitable_is_pro() ) : ?>
3038
3039 <button disabled="disabled" value="<?php echo esc_html__( 'Download CSV', 'charitable' ); ?>" class="button with-icon charitable-report-download-button" title="<?php echo esc_html__( 'Download CSV', 'charitable' ); ?>" data-nonce=""><label><?php echo esc_html__( 'Download CSV', 'charitable' ); ?></label><img src="<?php echo charitable()->get_path( 'assets', false ) . 'images/icons/download.svg'; // phpcs:ignore ?>" alt=""></button>
3040
3041 <?php else : ?>
3042
3043 <form action="" method="post" class="charitable-advanced-download-form" id="charitable-advanced-download-form">
3044 <input name="charitable_report_action" type="hidden" value="charitable_report_download_advanced">
3045 <input name="start_date_compare_from" type="hidden" value="<?php echo esc_html( $report_args['start_date_compare_from'] ); ?>">
3046 <input name="end_date_compare_from" type="hidden" value="<?php echo esc_html( $report_args['end_date_compare_from'] ); ?>">
3047 <input name="start_date_compare_to" type="hidden" value="<?php echo esc_html( $report_args['start_date_compare_to'] ); ?>">
3048 <input name="end_date_compare_to" type="hidden" value="<?php echo esc_html( $report_args['end_date_compare_to'] ); ?>">
3049 <input name="campaign_id" type="hidden" value="-1">
3050 <input name="report_type" type="hidden" value="<?php echo esc_html( $report_type ); ?>">
3051 <?php wp_nonce_field( 'charitable_export_report', 'charitable_export_report_nonce' ); ?>
3052 <button value="<?php echo esc_html__( 'Download CSV', 'charitable' ); ?>" type="submit" class="button with-icon charitable-report-download-button" title="<?php echo esc_html__( 'Download CSV', 'charitable' ); ?>" data-nonce=""><label><?php echo esc_html__( 'Download CSV', 'charitable' ); ?></label><img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) . 'images/icons/download.svg' ); ?>" alt=""></button>
3053 </form>
3054
3055 <?php endif; ?>
3056
3057 </div>
3058
3059 </div>
3060
3061 <br class="clear">
3062
3063 <div class="charitable-report-table-container">
3064
3065 <?php do_action( 'charitable_report_before_advanced_table' ); ?>
3066
3067 <?php
3068 if ( ! charitable_is_pro() ) :
3069 ?>
3070 <div class="charitable-restricted"><div class="restricted-access-overlay"></div><?php endif; ?>
3071
3072 <table class="wp-list-table widefat fixed striped table-view-list donations-breakdown charitable-report-ui">
3073 <thead>
3074 <tr>
3075 <?php
3076 if ( ! empty( $table_meta['columns'] ) ) :
3077 foreach ( $table_meta['columns'] as $column_slug => $column_title ) :
3078 if ( $column_slug === 'avatar' ) {
3079 echo '<th style="width: 32px;" class="charitable-avatar"></th>';
3080 } else {
3081 echo '<th scope="col" id="' . esc_attr( $column_slug ) . '" class="manage-column column-' . esc_attr( $column_slug ) . '"><span>' . wp_kses( $column_title, $title_whitelist_tags ) . '</span></th>';
3082 }
3083 endforeach;
3084 endif;
3085 ?>
3086 </tr>
3087 </thead>
3088 <tbody id="donations-breakdown-list">
3089 <?php echo $this->generate_advanced_breakdown_rows( $advanced_report_data, $report_type, count( $table_meta['columns'], $show_actions ) ); // phpcs:ignore ?>
3090 </tbody>
3091 <tfoot>
3092 <tr>
3093 <?php
3094 foreach ( $table_meta['columns'] as $column_slug => $column_title ) :
3095 if ( $column_slug === 'avatar' ) {
3096 echo '<th style="width: 32px;" class="charitable-avatar"></th>';
3097 } else {
3098 echo '<th scope="col" id="' . esc_html( $column_slug ) . '" class="manage-column column-' . esc_html( $column_slug ) . '"><span>' . wp_kses( $column_title, $title_whitelist_tags ) . '</span></th>';
3099 }
3100 endforeach;
3101 ?>
3102 </tr>
3103 </tfoot>
3104 </table>
3105
3106 <?php
3107 if ( ! charitable_is_pro() ) :
3108 ?>
3109 </div><?php endif; ?>
3110
3111 <?php
3112
3113 do_action( 'charitable_report_after_advanced_table' );
3114
3115 ?>
3116 </div>
3117 <?php
3118
3119 return ob_get_clean();
3120 }
3121
3122 /**
3123 * Determines what function to generate table rows for advanced based on the report type.
3124 *
3125 * @since 1.8.1
3126 *
3127 * @param array $advanced_report_data The report data.
3128 * @param string $report_type The report type.
3129 * @param int $number_of_columns The number of columns.
3130 * @param bool $show_actions Whether to show actions.
3131 *
3132 * @return string
3133 */
3134 public function generate_advanced_breakdown_rows( $advanced_report_data = array(), $report_type = 'lybunt', $number_of_columns = false, $show_actions = false ) {
3135
3136 $rows = false;
3137
3138 switch ( $report_type ) {
3139 case 'lybunt':
3140 $rows = $this->generate_lybunt_rows( $advanced_report_data, $number_of_columns, $show_actions );
3141 break;
3142
3143 default:
3144 // code...
3145 break;
3146 }
3147
3148 return $rows;
3149 }
3150
3151 /**
3152 * Generates HTML for the "report cards" at the top of the advanced page.
3153 *
3154 * @since 1.8.1
3155 *
3156 * @param array $advanced_report_data The report data.
3157 * @param string $report_type The report type.
3158 * @param array $report_args The report args.
3159 *
3160 * @return string
3161 */
3162 public function generate_advanced_cards( $advanced_report_data = array(), $report_type = 'lybunt', $report_args = array() ) {
3163
3164 ob_start();
3165
3166 if ( 'lybunt' === $report_type ) :
3167
3168 if ( charitable_is_pro() ) :
3169
3170 if ( ! is_array( $advanced_report_data ) ) {
3171 $advanced_report_data = array();
3172 }
3173
3174 // count all elements in a multi-dimenstional array that has a particular value greator than 0.
3175 $total_count_donors_from = number_format(
3176 count(
3177 array_filter(
3178 array_column( $advanced_report_data, 'total_donation_count_compare_from' ),
3179 function ( $value ) {
3180 return $value > 0;
3181 }
3182 )
3183 )
3184 );
3185 $total_count_donors_to = number_format(
3186 count(
3187 array_filter(
3188 array_column( $advanced_report_data, 'total_donation_count_compare_to' ),
3189 function ( $value ) {
3190 return $value > 0;
3191 }
3192 )
3193 )
3194 );
3195
3196 $total_amount_donations_from = array_sum( array_column( $advanced_report_data, 'total_donation_amount_compare_from' ) );
3197 $total_amount_donations_to = array_sum( array_column( $advanced_report_data, 'total_donation_amount_compare_to' ) );
3198
3199 // if value does not have decimals, then add two decimal places.
3200 $total_amount_donations_from = ( false === strpos( $total_amount_donations_from, '.' ) ) ? $total_amount_donations_from . '.00' : $total_amount_donations_from;
3201 $total_amount_donations_to = ( false === strpos( $total_amount_donations_to, '.' ) ) ? $total_amount_donations_to . '.00' : $total_amount_donations_to;
3202
3203 $from_date_range = '( ' . $report_args['start_date_compare_from'] . ' - ' . $report_args['end_date_compare_from'] . ' )';
3204 $to_date_range = '( ' . $report_args['start_date_compare_to'] . ' - ' . $report_args['end_date_compare_to'] . ' )';
3205
3206 else :
3207
3208 $total_count_donors_from = number_format( 143 );
3209 $total_count_donors_to = number_format( 1332 );
3210 $total_amount_donations_from = '$500.00';
3211 $total_amount_donations_to = '$25,040.00';
3212 $from_date_range = '2022/01/01 - 2022/12/31';
3213 $to_date_range = '2023/01/01 - 2023/12/31';
3214
3215 endif;
3216
3217
3218 ?>
3219
3220 <div class="charitable-cards">
3221 <div class="charitable-container charitable-report-ui charitable-card">
3222 <strong><span id="charitable-top-donor-count"><?php echo ( intval( $total_count_donors_from ) ); ?></span></strong>
3223 <p><?php echo esc_html__( 'Donors', 'charitable' ); ?></p>
3224 <p><?php echo esc_html( $from_date_range ); ?></p>
3225 </div>
3226 <div class="charitable-container charitable-report-ui charitable-card">
3227 <strong><span id="charitable-top-donor-count"><?php echo ( intval( $total_count_donors_to ) ); ?></span></strong>
3228 <p><?php echo esc_html__( 'Donors', 'charitable' ); ?></p>
3229 <p><?php echo esc_html( $to_date_range ); ?></p>
3230 </div>
3231 <div class="charitable-container charitable-report-ui charitable-card">
3232 <strong><span id="charitable-top-donor-count"><?php echo charitable_format_money( $total_amount_donations_from, 2, true ); // phpcs:ignore ?></span></strong>
3233 <p><?php echo esc_html__( 'Donations', 'charitable' ); ?></p>
3234 <p><?php echo esc_html( $from_date_range ); ?></p>
3235 </div>
3236 <div class="charitable-container charitable-report-ui charitable-card">
3237 <strong><span id="charitable-top-donor-count"><?php echo charitable_format_money( $total_amount_donations_to, 2, true ); // phpcs:ignore ?></span></strong>
3238 <p><?php echo esc_html__( 'Donations', 'charitable' ); ?></p>
3239 <p><?php echo esc_html( $to_date_range ); ?></p>
3240 </div>
3241 </div>
3242
3243 <?php
3244
3245 endif;
3246
3247 return ob_get_clean();
3248 }
3249
3250 /**
3251 * Generates HTML for the "report table" at the middle of the advanced page.
3252 *
3253 * @since 1.8.1
3254 *
3255 * @param array $report_type The report type.
3256 * @param array $report_args The report args.
3257 * @param array $show_actions Whether to show actions.
3258 *
3259 * @return string
3260 */
3261 public function get_advanced_report_table_meta( $report_type = 'lybunt', $report_args = array(), $show_actions = false ) {
3262
3263 $table_meta = array();
3264
3265 switch ( $report_type ) {
3266 case 'lybunt':
3267 $number_donations_1_date_range = ! empty( $report_args['start_date_compare_from'] ) && ! empty( $report_args['end_date_compare_from'] ) ? esc_html( $report_args['start_date_compare_from'] ) . ' - ' . esc_html( $report_args['end_date_compare_from'] ) : '';
3268 $number_donations_2_date_range = ! empty( $report_args['start_date_compare_to'] ) && ! empty( $report_args['end_date_compare_to'] ) ? esc_html( $report_args['start_date_compare_to'] ) . ' - ' . esc_html( $report_args['end_date_compare_to'] ) : '';
3269
3270 $table_meta = array(
3271 'title' => esc_html__( 'Donors', 'charitable' ),
3272 'columns' => array(
3273 'avatar' => '',
3274 'name-email' => esc_html__( 'Name', 'charitable' ),
3275 'total-donations' => esc_html__( 'Last Donation', 'charitable' ),
3276 'number-donations-1' => esc_html__( 'Total Donations', 'charitable' ) . '<br/>' . $number_donations_1_date_range,
3277 'number-donations-2' => esc_html__( 'Total Donations', 'charitable' ) . '<br/>' . $number_donations_2_date_range,
3278 'last-campaign' => esc_html__( 'Last Campaign', 'charitable' ),
3279 'life-total' => esc_html__( 'Life Total', 'charitable' ),
3280 ),
3281 );
3282
3283 if ( $show_actions ) {
3284 $table_meta['columns']['actions'] = esc_html__( 'Actions', 'charitable' );
3285 }
3286
3287 break;
3288
3289 default:
3290 // code...
3291 break;
3292 }
3293
3294 return $table_meta;
3295 }
3296
3297 /**
3298 * This gets report args and an iniital report.
3299 *
3300 * @param string $report_args The report args.
3301 *
3302 * @since 1.8.1
3303 *
3304 * @return array
3305 */
3306 public function get_lybunt_data( $report_args = array() ) {
3307
3308 global $wpdb;
3309
3310 $start_date_compare_from = esc_html( $report_args['start_date_compare_from'] );
3311 $end_date_compare_from = esc_html( $report_args['end_date_compare_from'] );
3312 $start_date_compare_to = esc_html( $report_args['start_date_compare_to'] );
3313 $end_date_compare_to = esc_html( $report_args['end_date_compare_to'] );
3314 $limit = ! empty( $report_args['limit'] ) ? intval( $report_args['limit'] ) : apply_filters( 'charitable_report_advanced_lybunt_limit', 100 );
3315 $order_by = ! empty( $report_args['order_by'] ) ? esc_html( $report_args['order_by'] ) : 'total_life_amount';
3316 $order = ! empty( $report_args['order'] ) ? esc_html( $report_args['order'] ) : 'DESC';
3317
3318 $_donors = array();
3319
3320 // Determine if this particular report has been cached recently.
3321 // For now though we are not caching.
3322 $donors = $this->maybe_cache_report( false ) ? false : $this->get_cached_report( 'lybunt', $start_date_compare_from, $end_date_compare_to, false, false, false );
3323
3324 if ( false === $donors ) {
3325
3326 // Get the foundation of the report.
3327 $donors_compare_from = (array) $this->get_donors_from_db( 'compare_from', $start_date_compare_from, $end_date_compare_from, 'array' );
3328 $donors_compare_to = (array) $this->get_donors_from_db( 'compare_to', $start_date_compare_to, $end_date_compare_to, 'array' );
3329
3330 // Merge the two arrays - from and to.
3331 if ( ! empty( $donors_compare_from ) ) :
3332
3333 foreach ( $donors_compare_from as $key => $donor ) :
3334 $_donors[ $donor['donor_id'] ] = $donor;
3335 endforeach;
3336
3337 endif;
3338
3339 if ( ! empty( $donors_compare_to ) ) :
3340
3341 foreach ( $donors_compare_to as $key => $donor ) :
3342 // if donor_id exists in $_donors, merge the two arrays.
3343 if ( is_array( $_donors ) && array_key_exists( $donor['donor_id'], $_donors ) ) {
3344 $_donors[ $donor['donor_id'] ] = array_merge( $_donors[ $donor['donor_id'] ], $donor );
3345 } else {
3346 $_donors[ $donor['donor_id'] ] = $donor;
3347 }
3348 endforeach;
3349
3350 endif;
3351
3352 if ( empty( $_donors ) ) {
3353 return false;
3354 }
3355
3356 // Convert array of arrays to array of objects, keeping the array keys.
3357 $_donors = array_map(
3358 function ( $donor ) {
3359 return (object) $donor;
3360 },
3361 $_donors
3362 );
3363
3364 $donors = array();
3365
3366 foreach ( $_donors as $key => $donor ) {
3367 if ( ! empty( $donor->donor_id ) && 0 !== intval( $donor->donor_id ) ) :
3368 $donors[ $donor->donor_id ] = $donor;
3369 endif;
3370 }
3371
3372 foreach ( $donors as $donor_id => $donor_info ) {
3373
3374 $donor = new Charitable_Donor( $donor_id );
3375 $last_donation = $donor->get_last_donation();
3376 $last_donation_amount = $last_donation->get_total_donation_amount();
3377 $last_donation_campaign_links = $last_donation->get_campaigns_links();
3378 $donors[ $donor_id ]->last_donation_amount = $last_donation_amount;
3379 $donors[ $donor_id ]->last_donation_campaign_links = $last_donation_campaign_links;
3380 $donors[ $donor_id ]->total_life_amount = $donor->get_amount();
3381
3382 if ( empty( $donors[ $donor_id ]->total_donation_count_compare_from ) ) {
3383 $donors[ $donor_id ]->total_donation_count_compare_from = 0;
3384 }
3385 if ( empty( $donors[ $donor_id ]->total_donation_amount_compare_from ) ) {
3386 $donors[ $donor_id ]->total_donation_amount_compare_from = 0;
3387 }
3388 if ( empty( $donors[ $donor_id ]->total_donation_count_compare_to ) ) {
3389 $donors[ $donor_id ]->total_donation_count_compare_to = 0;
3390 }
3391 if ( empty( $donors[ $donor_id ]->total_donation_amount_compare_to ) ) {
3392 $donors[ $donor_id ]->total_donation_amount_compare_to = 0;
3393 }
3394 }
3395
3396 // Sort the data.
3397 if ( is_array( $donors ) && count( $donors ) > 0 ) :
3398 $donors = $this->sort_lybunt_data( $donors, $order_by, $order );
3399 endif;
3400
3401 // Reduce the array to the limit.
3402 if ( $limit && is_array( $donors ) && count( $donors ) > $limit ) :
3403 $donors = array_slice( $donors, 0, $limit, true );
3404 endif;
3405
3406 // Save this to the cache.
3407 $this->set_cached_report( 'lybunt', $start_date_compare_from, $end_date_compare_to, false, false, false, $donors );
3408
3409 }
3410
3411 return $donors;
3412 }
3413
3414 /**
3415 * Sorts the lybunt data.
3416 *
3417 * @since 1.8.1.3
3418 *
3419 * @param array $donors The donors data.
3420 * @param string $order_by The order by. Default is total_life_amount.
3421 * @param string $order The order (DESC OR ASC). Default is DESC.
3422 *
3423 * @return array
3424 */
3425 public function sort_lybunt_data( $donors = array(), $order_by = 'total_life_amount', $order = 'DESC' ) {
3426
3427 if ( ! empty( $donors ) ) :
3428
3429 usort(
3430 $donors,
3431 function ( $a, $b ) use ( $order_by, $order ) {
3432 if ( $order === 'DESC' && ! empty( $a->$order_by ) && ! empty( $b->$order_by ) ) {
3433 return $b->$order_by <=> $a->$order_by;
3434 } elseif ( $order === 'ASC' && ! empty( $a->$order_by ) && ! empty( $b->$order_by ) ) {
3435 return $a->$order_by <=> $b->$order_by;
3436 } else {
3437 return;
3438 }
3439 }
3440 );
3441
3442 endif;
3443
3444 return $donors;
3445 }
3446
3447 /**
3448 * Generates HTML rows for doantion breakdown table.
3449 *
3450 * @since 1.8.1
3451 *
3452 * @param array $donors_data The donors data.
3453 * @param int $number_of_columns The number of columns.
3454 * @param bool $show_actions Whether to show actions.
3455 *
3456 * @return string
3457 */
3458 public function generate_lybunt_rows( $donors_data = array(), $number_of_columns = false, $show_actions = false ) {
3459
3460 ob_start();
3461
3462 if ( empty( $donors_data ) ) {
3463 ?>
3464
3465 <td colspan="<?php echo intval( $number_of_columns ); ?>"><strong><?php echo esc_html__( 'There are no donations within the date range.', 'charitable' ); ?></strong></td>
3466
3467 <?php
3468
3469 } else {
3470
3471 foreach ( $donors_data as $donor_id => $data ) :
3472
3473 $donor_name = $data->donor_first_name . ' ' . $data->donor_last_name;
3474 $donor_email = $data->donor_email;
3475 $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . 'images/misc/placeholder-avatar-small.jpg';
3476 $total_donation_count_compare_to = $data->total_donation_count_compare_to;
3477 $total_donation_amount_compare_to = number_format( $data->total_donation_amount_compare_to, 2 );
3478 $total_donation_count_compare_from = $data->total_donation_count_compare_from;
3479 $total_donation_amount_compare_from = number_format( $data->total_donation_amount_compare_from, 2 );
3480 $last_donation_amount = $data->last_donation_amount;
3481 $total_life_amount = number_format( $data->total_life_amount, 2 );
3482 $campaign_links = implode( ',', $data->last_donation_campaign_links );
3483 $link = false;
3484 $actions_display = '';
3485
3486 ?>
3487
3488 <tr id="type-lybunt-donor-<?php echo intval( $donor_id ); ?>" class="row-type-lybunt">
3489 <td class="charitable-avatar" style="width: 32px;"><img src="<?php echo esc_url( $donor_avatar ); ?>" class="avatar avatar-<?php echo esc_html( $donor_id ); ?> photo" width="32" height="32" alt="<?php esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>"></td>
3490 <td class="donated column-date" data-colname="<?php echo esc_html__( 'Date.', 'charitable' ); ?>"><?php echo esc_html( $donor_name ); ?><br/><?php echo esc_html( $donor_email ); ?></td>
3491 <td class="donated column-donations" data-colname="Donations"><?php echo charitable_format_money( $last_donation_amount ); // phpcs:ignore ?></td>
3492 <td class="donated column-donors" data-colname="Number of Donors"><?php echo esc_html( $total_donation_count_compare_from ); ?> <?php echo esc_html__( 'Donations', 'charitable' ); ?><br/><small><?php echo charitable_format_money( $total_donation_amount_compare_from, 2, true ); // phpcs:ignore ?></small></td>
3493 <td class="donated column-donors" data-colname="Number of Donors"><?php echo esc_html( $total_donation_count_compare_to ); ?> <?php echo esc_html__( 'Donations', 'charitable' ); ?><br/><small><?php echo charitable_format_money( $total_donation_amount_compare_to, 2, true ); // phpcs:ignore ?></small></td>
3494 <td class="donated column-refunds" data-colname="Refunds"><?php echo $campaign_links; // phpcs:ignore ?></td>
3495 <td class="donated column-net " data-colname="Net"><?php echo charitable_format_money( $total_life_amount, 2, true ); // phpcs:ignore ?></td>
3496 <?php
3497
3498 if ( $show_actions ) :
3499 if ( $link && charitable_is_pro() ) :
3500
3501 $actions_display = '<td class="donated column-actions " data-colname="Actions"><a class="charitable-campaign-action-button" title="' . esc_html__( 'View User', 'charitable' ) . '" href="' . esc_url( $link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . 'images/icons/eye.svg" width="14" height="14" alt="' . esc_html__( 'View User', 'charitable' ) . '" /></td>';
3502
3503 else :
3504
3505 $actions_display = '<td class="donated column-actions " data-colname="Actions"><a class="charitable-campaign-action-button" title="' . esc_html__( 'View User', 'charitable' ) . '" href="#" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . 'images/icons/eye.svg" width="14" height="14" alt="' . esc_html__( 'View User', 'charitable' ) . '" /></td>';
3506
3507 endif;
3508 endif;
3509
3510 echo $actions_display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
3511 ?>
3512
3513 </tr>
3514
3515 <?php
3516 endforeach;
3517
3518 }
3519
3520 return ob_get_clean();
3521 }
3522
3523 /**
3524 * Return the HTML of the filter select dropdown.
3525 *
3526 * @since 1.8.1
3527 *
3528 * @param string $selected_value The selected value.
3529 *
3530 * @return string
3531 */
3532 public function get_advanced_report_type_dropdown( $selected_value = false ) {
3533
3534 $options_values = apply_filters(
3535 'charitable_reports_advanced_report_type_filter_values',
3536 array(
3537 'lybunt' => __( 'LYBUNT / SYBUNT', 'charitable' ),
3538 )
3539 );
3540
3541 ob_start();
3542
3543 ?>
3544
3545 <select autocomplete="off" name="action" id="report-advanced-type-filter">
3546 <?php foreach ( $options_values as $value => $label ) : ?>
3547 <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_value, $value ); ?>><?php echo esc_html( $label ); ?></option>
3548 <?php endforeach; ?>
3549 </select>
3550
3551 <?php
3552
3553 return ob_get_clean();
3554 }
3555
3556 /**
3557 * Get the donors for the dedicated advanced report.
3558 *
3559 * @since 1.8.1
3560 *
3561 * @param string $report_args The report args.
3562 *
3563 * @return array|bool
3564 */
3565 public function get_advanced_data_by_report_type( $report_args = array() ) {
3566
3567 if ( empty( $report_args['report_type'] ) ) {
3568 return false;
3569 }
3570
3571 switch ( sanitize_title( $report_args['report_type'] ) ) {
3572 case 'lybunt':
3573 return ( $this->get_lybunt_data( $report_args ) );
3574 break; // phpcs:ignore
3575
3576 default:
3577 return false;
3578 break; // phpcs:ignore
3579 }
3580 }
3581
3582 /**
3583 * Return net value
3584 *
3585 * @since 1.8.1
3586 *
3587 * @param float $donation_amount The donation amount.
3588 * @param float $refund_amount The refund amount.
3589 *
3590 * @return float
3591 */
3592 public function get_net( $donation_amount = 0, $refund_amount = 0 ) {
3593
3594 // If the donation amount is less than the refund amount, return 0.
3595 if ( $donation_amount < $refund_amount ) {
3596 return 0;
3597 }
3598
3599 return $donation_amount - $refund_amount;
3600 }
3601
3602 /**
3603 * Amount formatting for reports - includes currency symbol.
3604 *
3605 * @since 1.8.1
3606 *
3607 * @param int $amount The amount.
3608 *
3609 * @return string
3610 */
3611 public function charitable_reports_format_money( $amount = 0 ) {
3612
3613 return html_entity_decode( charitable_format_money( $amount ) );
3614 }
3615
3616 /**
3617 * Returns if activities are empty.
3618 *
3619 * @since 1.8.1
3620 *
3621 * @return boolean
3622 */
3623 public function is_activities_empty() {
3624
3625 if ( $this->activities === false || empty( $this->activities ) ) {
3626 return true;
3627 }
3628
3629 return false;
3630 }
3631
3632 /**
3633 * Get the date of the earliest donation on the site.
3634 *
3635 * Used to build the "All Time" preset in the reporting date picker, so that
3636 * lifetime figures are one click away rather than a hand-typed range.
3637 *
3638 * Every valid donation status is considered, not just completed ones, because
3639 * the reports can be filtered by status and a narrower floor would hide the
3640 * earliest refunded or cancelled donation.
3641 *
3642 * @since 1.8.12.4
3643 *
3644 * @return string Date in Y/m/d format. Falls back to one month ago when the
3645 * site has no donations yet.
3646 */
3647 public function get_earliest_donation_date() {
3648
3649 $cached = get_transient( 'charitable-reports-earliest-donation-date' );
3650
3651 if ( false !== $cached ) {
3652 return $cached;
3653 }
3654
3655 global $wpdb;
3656
3657 $statuses = array_keys( charitable_get_valid_donation_statuses() );
3658
3659 // `charitable_donation_statuses` is a public filter, and an empty list would
3660 // build `IN ( )`. Nothing to measure in that case, so use the fallback.
3661 if ( empty( $statuses ) ) {
3662 return gmdate( 'Y/m/d', strtotime( '-1 month' ) );
3663 }
3664
3665 $placeholders = implode( ', ', array_fill( 0, count( $statuses ), '%s' ) );
3666
3667 $earliest = $wpdb->get_var( // phpcs:ignore
3668 $wpdb->prepare(
3669 "SELECT MIN( post_date ) FROM {$wpdb->posts}
3670 WHERE post_type = 'donation'
3671 AND post_status IN ( {$placeholders} )", // phpcs:ignore
3672 $statuses
3673 )
3674 );
3675
3676 if ( empty( $earliest ) || '0000-00-00 00:00:00' === $earliest ) {
3677 /*
3678 * Deliberately not cached. A site with no donations today may be one
3679 * import away from four years of history, and a cached fallback would
3680 * leave "All Time" starting a month ago for a full day afterwards -
3681 * which is the exact wrong-figure trap this preset exists to close.
3682 */
3683 return gmdate( 'Y/m/d', strtotime( '-1 month' ) );
3684 }
3685
3686 $date = gmdate( 'Y/m/d', strtotime( $earliest ) );
3687
3688 set_transient( 'charitable-reports-earliest-donation-date', $date, DAY_IN_SECONDS );
3689
3690 return $date;
3691 }
3692
3693 /**
3694 * Get localized strings.
3695 *
3696 * @since 1.8.1
3697 *
3698 * @return array
3699 */
3700 private function get_localized_strings() {
3701
3702 $charitable_reports_advanced_report_strings = apply_filters(
3703 'charitable_reports_advanced_report_strings',
3704 array(
3705 'lybunt' => array(
3706 'title' => __( 'LYBUNT / SYBUNT', 'charitable' ),
3707 'description' => wp_kses_post(
3708 __(
3709 '<p>LYBUNT stands for <strong>Last Year But Unfortunately Not This Year</strong>, meaning the donor supported your organization during the last fiscal year but did not return to donate again this year. SYBUNT stands for <strong>Some Year But Unfortunately Not This Year</strong>. </p>
3710
3711 <p><strong>Why This Is Important:</strong> LYBUNT and SYBUNT reports are important to nonprofits because they allow you to look for low-hanging fruit. Nonprofit organizations have an average retention rate of 43.6%. In order to meet or exceed this retention rate, it is important to nurture the donors you have.</p>',
3712 'charitable'
3713 ),
3714 array(
3715 'p' => array(),
3716 'strong' => array(),
3717 )
3718 ),
3719 ),
3720 )
3721 );
3722
3723 $currency_helper = charitable_get_currency_helper();
3724
3725 // Decode HTML entities in currency symbol to prevent double-encoding in JSON.
3726 // The currency symbol comes as HTML entities (e.g., &#36; for $), so we decode it
3727 // before passing to JavaScript to avoid double-encoding issues.
3728 // This ensures the currency symbol is a plain character that won't be double-encoded.
3729 $currency_symbol = $currency_helper->get_currency_symbol();
3730 // Decode HTML entities to get the plain character (e.g., &#36; becomes $).
3731 $currency_symbol = html_entity_decode( $currency_symbol, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
3732 $decimal_count = $currency_helper->get_decimals();
3733
3734 $strings = array(
3735 'version' => Charitable::VERSION,
3736 'nonce' => wp_create_nonce( 'charitable-reporting' ),
3737 'admin_nonce' => wp_create_nonce( 'charitable-admin' ),
3738 'currency_symbol' => $currency_symbol,
3739 'decimal_count' => (int) $decimal_count,
3740 'ajax_url' => admin_url( 'admin-ajax.php' ),
3741 'date_select_day' => 'DD',
3742 'date_select_month' => 'MM',
3743 'headline_chart_options' => array(
3744 'donation_axis' => (array) $this->donation_axis,
3745 'date_axis' => (array) $this->date_axis,
3746 ),
3747 'payment_methods_chart_options' => array(
3748 'payment_percentages' => (array) array_reverse( $this->payment_percentages ),
3749 'payment_labels' => (array) array_reverse( $this->payment_labels ),
3750 'payment_keys' => (array) array_reverse( $this->payment_keys ),
3751 ),
3752 'default_start_date' => current_time( gmdate( 'Y-m-d', strtotime( '-1 month' ) ) ),
3753 'default_end_date' => current_time( gmdate( 'Y-m-d' ) ),
3754 'earliest_donation_date' => $this->get_earliest_donation_date(),
3755 // The date picker uses each preset's label as its object key, so the
3756 // label has to travel to JS rather than being written there. It rides
3757 // this same array as `earliest_donation_date` on purpose: the
3758 // reporting JS contains no wp.i18n calls, so a second localization
3759 // mechanism for one string would be the inconsistent choice.
3760 'all_time_label' => __( 'All Time', 'charitable' ),
3761 'advanced_reports' => $charitable_reports_advanced_report_strings,
3762 'i18n_report_timeout' => __( 'This report took too long to load and was stopped by the server. The figures have been cleared so they are not mistaken for the range you asked for. Try a shorter date range.', 'charitable' ),
3763 'i18n_report_error' => __( 'This report could not be loaded. The figures have been cleared so they are not mistaken for the range you asked for. Please try again.', 'charitable' ),
3764 );
3765
3766 $strings = apply_filters( 'charitable_reporting_strings', $strings );
3767
3768 return $strings;
3769 }
3770
3771 /**
3772 * Return the array of tabs used on the settings page.
3773 *
3774 * @since 1.8.1
3775 *
3776 * @return string[]
3777 */
3778 public function get_sections() {
3779 /**
3780 * Filter the settings tabs.
3781 *
3782 * @since 1.8.1
3783 *
3784 * @param string[] $tabs List of tabs in key=>label format.
3785 */
3786 return apply_filters(
3787 'charitable_reports_tabs',
3788 array(
3789 'overview' => __( 'Overview', 'charitable' ),
3790 'advanced' => __( 'Advanced', 'charitable' ),
3791 'activity' => __( 'Activity', 'charitable' ),
3792 'donors' => __( 'Donors', 'charitable' ),
3793 'analytics' => __( 'Analytics', 'charitable' ),
3794 )
3795 );
3796 }
3797
3798 /**
3799 * Return the array of terms for custom campaign categories.
3800 *
3801 * @since 1.8.1
3802 *
3803 * @return string[]
3804 */
3805 public function get_campaign_categories() {
3806
3807 return apply_filters(
3808 'charitable_reports_filter_campaign_categories',
3809 get_terms(
3810 'campaign_category'
3811 )
3812 );
3813 }
3814
3815 /**
3816 * Provides the content (title/description) to display inside the title card for a particular report.
3817 *
3818 * @since 1.8.1
3819 *
3820 * @param string $report_type The report type.
3821 *
3822 * @return string[]
3823 */
3824 public function generate_title_card_html( $report_type = false ) {
3825
3826 if ( false === $report_type ) {
3827 return;
3828 }
3829
3830 switch ( sanitize_title( $report_type ) ) {
3831 case 'donors-top':
3832 $title = __( 'Top Donors', 'charitable' );
3833 // use wp_kses_post to allow html in the string.
3834 $description = wp_kses_post(
3835 __(
3836 '<p>This report shows which donors have made the largest gifts and donations.</p>
3837 <p><strong>Why This Is Important:</strong> It will show you which donors you can turn to when you need to make an ask for major needs or projects. Given that they are donating a significant amount of money at one time, you don’t want to lump them in with campaigns asking people for smaller, one-off donations. This can help you identify donors who are increasing their giving year over year and who may be good candidates for a major gift ask.</p>
3838 <p>You can also use this report to thank top donors for their support.</p>',
3839 'charitable'
3840 ),
3841 array(
3842 'p' => array(),
3843 'strong' => array(),
3844 )
3845 );
3846 break;
3847
3848 case 'donors-recurring':
3849 $title = __( 'Recurring Donors', 'charitable' );
3850 // use wp_kses_post to allow html in the string.
3851 $description = wp_kses_post(
3852 __(
3853 '<p>This report shows which donors have made repeated donations the largest gifts and donations.</p>
3854 <p><strong>Why This Is Important:</strong> It will show you which donors you can turn to when you need to make an ask for major needs or projects. Sometimes you do not want to target all donors - especially those that have given once or in small amounts - in repeated campaigns.</p>
3855 <p>You can also use this report to thank recurring donors for their support.</p>',
3856 'charitable'
3857 ),
3858 array(
3859 'p' => array(),
3860 'strong' => array(),
3861 )
3862 );
3863 break;
3864
3865 case 'donors-first-time':
3866 $title = __( 'First Time Donors', 'charitable' );
3867 // use wp_kses_post to allow html in the string.
3868 $description = wp_kses_post(
3869 __(
3870 '<p>This report shows which donors have made one donation - their first.</p>
3871 <p><strong>Why This Is Important:</strong> Seeing which donors are new to your campaigns can help you determine where your traffic is coming from or how donors are finding you.</p>
3872 <p>You can also use this report to thank first time donors for their support.</p>',
3873 'charitable'
3874 ),
3875 array(
3876 'p' => array(),
3877 'strong' => array(),
3878 )
3879 );
3880 break;
3881
3882 case 'lybunt':
3883 $title = __( 'LYBUNT / SYBUNT', 'charitable' );
3884 $description = wp_kses_post(
3885 __(
3886 '<p>LYBUNT stands for <strong>Last Year But Unfortunately Not This Year</strong>, meaning the donor supported your organization during the last fiscal year but did not return to donate again this year. SYBUNT stands for <strong>Some Year But Unfortunately Not This Year</strong>. </p>
3887
3888 <p><strong>Why This Is Important:</strong> LYBUNT and SYBUNT reports are important to nonprofits because they allow you to look for low-hanging fruit. Nonprofit organizations have an average retention rate of 43.6%. In order to meet or exceed this retention rate, it is important to nurture the donors you have.</p>',
3889 'charitable'
3890 ),
3891 array(
3892 'p' => array(),
3893 'strong' => array(),
3894 )
3895 );
3896 break;
3897
3898 default:
3899 // code...
3900 break;
3901 }
3902
3903 ob_start();
3904
3905 ?>
3906 <h2><?php echo esc_html( $title ); ?></h2>
3907 <div class="charitable-report-description"><?php echo $description; // phpcs:ignore ?></div>
3908 <?php
3909
3910 return ob_get_clean();
3911 }
3912
3913 /**
3914 * Return the HTML of the filter select dropdown.
3915 *
3916 * @since 1.8.1
3917 *
3918 * @param string $selected_value The selected value.
3919 *
3920 * @return string
3921 */
3922 public function get_filter_dropdown( $selected_value = false ) {
3923
3924 $options_values = $this->date_filter_values;
3925
3926 ob_start();
3927
3928 ?>
3929
3930 <select name="action" id="report-date-range-filter">
3931 <?php foreach ( $options_values as $value => $label ) : ?>
3932 <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_value, $value ); ?>><?php echo esc_html( $label ); ?></option>
3933 <?php endforeach; ?>
3934 </select>
3935
3936 <?php
3937
3938 return ob_get_clean();
3939 }
3940
3941 /**
3942 * Return sample data for the example tables.
3943 *
3944 * @since 1.8.1
3945 *
3946 * @param string $report_type The report type.
3947 * @param string $data_type The data type. Object or array.
3948 * @param int $number_of_records The number of records to return. Default 10.
3949 *
3950 * @return array
3951 */
3952 public function get_data_sample( $report_type = false, $data_type = 'object', $number_of_records = 10 ) {
3953
3954 if ( false === $report_type ) {
3955 return;
3956 }
3957
3958 if ( $data_type === 'object' ) {
3959 $data = new stdClass();
3960 } else {
3961 $data = array();
3962 }
3963
3964 if ( $this->is_report_type_donor( $report_type ) ) {
3965
3966 for ( $x = 0; $x < $number_of_records; $x++ ) {
3967
3968 if ( $data_type === 'object' ) {
3969
3970 $random_price = wp_rand( 50, 10000 ) / 10;
3971
3972 $data->{$x} = new stdClass();
3973
3974 $data->{$x}->total_amount = $random_price;
3975 $data->{$x}->average = $random_price;
3976
3977 $data->{$x}->total_count_donations = 1;
3978 $data->{$x}->total_count_campaigns = 1;
3979 $data->{$x}->last_donation_date = '2023-11-28 16:59:33';
3980 $data->{$x}->last_campaign_title = 'My New Campaign 1';
3981 $data->{$x}->last_campaign_id = 0;
3982 $data->{$x}->donor_id = 0;
3983 $data->{$x}->user_id = 0;
3984 $data->{$x}->email = 'example_donor_' . $x . '@wpcharitable.com';
3985 $data->{$x}->first_name = 'Roger';
3986 $data->{$x}->last_name = 'Wilco';
3987 $data->{$x}->date_joined = '2023-11-28 16:59:33';
3988 $data->{$x}->donation_id = 0;
3989 $data->{$x}->campaign_id = 0;
3990 $data->{$x}->amount = $random_price;
3991 $data->{$x}->sample = true;
3992
3993 } else {
3994
3995 $random_price_1 = mt_rand( 50, 1000 ) / 10; // phpcs:ignore
3996
3997 $data[] = array(
3998
3999 'total_amount' => $random_price_1,
4000 'average' => $random_price_1,
4001 'total_count_donations' => 1,
4002 'total_count_campaigns' => 1,
4003 'last_donation_date' => '2023-11-28 16:59:33',
4004 'last_campaign_title' => 'My New Campaign 1',
4005 'last_campaign_id' => 0,
4006 'donor_id' => 0,
4007 'user_id' => 0,
4008 'email' => 'example_donor_' . $x . '@wpcharitable.com',
4009 'first_name' => 'Roger',
4010 'last_name' => 'Wilco',
4011 'date_joined' => '2024-01-28 16:59:33',
4012 'donation_id' => 0,
4013 'campaign_id' => 0,
4014 'amount' => $random_price_1,
4015 'sample' => true,
4016 );
4017 }
4018 }
4019
4020 } elseif ( $report_type === 'advanced' ) {
4021
4022 $random_price_1 = wp_rand( 50, 10000 ) / 10;
4023 $random_price_2 = wp_rand( 50, 10000 ) / 10;
4024 $random_price_3 = wp_rand( 50, 10000 ) / 10;
4025 $random_price_4 = wp_rand( 50, 10000 ) / 10;
4026
4027 $data = array(
4028 1 => (object) array(
4029 'donor_id' => 1,
4030 'total_donation_count_compare_to' => 3,
4031 'total_donation_amount_compare_to' => $random_price_1,
4032 'user_id' => 0,
4033 'donor_email' => '[email protected]',
4034 'donor_first_name' => 'David',
4035 'donor_last_name' => 'Bisset',
4036 'last_donation_amount' => $random_price_2,
4037 'last_donation_campaign_links' => array(
4038 34 => 'Help the Children Campaign',
4039 ),
4040 'total_life_amount' => 678,
4041 'total_donation_count_compare_from' => 0,
4042 'total_donation_amount_compare_from' => 0,
4043 'sample' => true,
4044 ),
4045 2 => (object) array(
4046 'donor_id' => 2,
4047 'total_donation_count_compare_to' => 3,
4048 'total_donation_amount_compare_to' => $random_price_3,
4049 'user_id' => 0,
4050 'donor_email' => '[email protected]',
4051 'donor_first_name' => 'Sarah',
4052 'donor_last_name' => 'Smith',
4053 'last_donation_amount' => $random_price_4,
4054 'last_donation_campaign_links' => array(
4055 34 => 'Help the Children Campaign',
4056 ),
4057 'total_life_amount' => $random_price_3,
4058 'total_donation_count_compare_from' => 0,
4059 'total_donation_amount_compare_from' => 0,
4060 'sample' => true,
4061 ),
4062 3 => (object) array(
4063 'donor_id' => 1,
4064 'total_donation_count_compare_to' => 3,
4065 'total_donation_amount_compare_to' => $random_price_1,
4066 'user_id' => 0,
4067 'donor_email' => '[email protected]',
4068 'donor_first_name' => 'Jim',
4069 'donor_last_name' => 'Bisset',
4070 'last_donation_amount' => $random_price_2,
4071 'last_donation_campaign_links' => array(
4072 34 => 'Help the Children Campaign',
4073 ),
4074 'total_life_amount' => 678,
4075 'total_donation_count_compare_from' => 0,
4076 'total_donation_amount_compare_from' => 0,
4077 'sample' => true,
4078 ),
4079 4 => (object) array(
4080 'donor_id' => 2,
4081 'total_donation_count_compare_to' => 3,
4082 'total_donation_amount_compare_to' => $random_price_3,
4083 'user_id' => 0,
4084 'donor_email' => '[email protected]',
4085 'donor_first_name' => 'George',
4086 'donor_last_name' => 'Smith',
4087 'last_donation_amount' => $random_price_4,
4088 'last_donation_campaign_links' => array(
4089 34 => 'Help the Children Campaign',
4090 ),
4091 'total_life_amount' => $random_price_3,
4092 'total_donation_count_compare_from' => 0,
4093 'total_donation_amount_compare_from' => 0,
4094 'sample' => true,
4095 ),
4096 5 => (object) array(
4097 'donor_id' => 1,
4098 'total_donation_count_compare_to' => 3,
4099 'total_donation_amount_compare_to' => $random_price_1,
4100 'user_id' => 0,
4101 'donor_email' => '[email protected]',
4102 'donor_first_name' => 'Zoe',
4103 'donor_last_name' => 'Bisset',
4104 'last_donation_amount' => $random_price_2,
4105 'last_donation_campaign_links' => array(
4106 34 => 'Help the Children Campaign',
4107 ),
4108 'total_life_amount' => 678,
4109 'total_donation_count_compare_from' => 0,
4110 'total_donation_amount_compare_from' => 0,
4111 'sample' => true,
4112 ),
4113 6 => (object) array(
4114 'donor_id' => 2,
4115 'total_donation_count_compare_to' => 3,
4116 'total_donation_amount_compare_to' => $random_price_3,
4117 'user_id' => 0,
4118 'donor_email' => '[email protected]',
4119 'donor_first_name' => 'Ava',
4120 'donor_last_name' => 'Smith',
4121 'last_donation_amount' => $random_price_4,
4122 'last_donation_campaign_links' => array(
4123 34 => 'Help the Children Campaign',
4124 ),
4125 'total_life_amount' => $random_price_3,
4126 'total_donation_count_compare_from' => 0,
4127 'total_donation_amount_compare_from' => 0,
4128 'sample' => true,
4129 ),
4130 7 => (object) array(
4131 'donor_id' => 1,
4132 'total_donation_count_compare_to' => 3,
4133 'total_donation_amount_compare_to' => $random_price_1,
4134 'user_id' => 0,
4135 'donor_email' => '[email protected]',
4136 'donor_first_name' => 'Zoe',
4137 'donor_last_name' => 'Bisset',
4138 'last_donation_amount' => $random_price_2,
4139 'last_donation_campaign_links' => array(
4140 34 => 'Help the Children Campaign',
4141 ),
4142 'total_life_amount' => 678,
4143 'total_donation_count_compare_from' => 0,
4144 'total_donation_amount_compare_from' => 0,
4145 'sample' => true,
4146 ),
4147 8 => (object) array(
4148 'donor_id' => 2,
4149 'total_donation_count_compare_to' => 3,
4150 'total_donation_amount_compare_to' => $random_price_3,
4151 'user_id' => 0,
4152 'donor_email' => '[email protected]',
4153 'donor_first_name' => 'Ava',
4154 'donor_last_name' => 'Smith',
4155 'last_donation_amount' => $random_price_4,
4156 'last_donation_campaign_links' => array(
4157 34 => 'Help the Children Campaign',
4158 ),
4159 'total_life_amount' => $random_price_3,
4160 'total_donation_count_compare_from' => 0,
4161 'total_donation_amount_compare_from' => 0,
4162 'sample' => true,
4163 ),
4164 9 => (object) array(
4165 'donor_id' => 1,
4166 'total_donation_count_compare_to' => 3,
4167 'total_donation_amount_compare_to' => $random_price_1,
4168 'user_id' => 0,
4169 'donor_email' => '[email protected]',
4170 'donor_first_name' => 'David',
4171 'donor_last_name' => 'Bisset',
4172 'last_donation_amount' => $random_price_2,
4173 'last_donation_campaign_links' => array(
4174 34 => 'Help the Children Campaign',
4175 ),
4176 'total_life_amount' => 678,
4177 'total_donation_count_compare_from' => 0,
4178 'total_donation_amount_compare_from' => 0,
4179 'sample' => true,
4180 ),
4181 10 => (object) array(
4182 'donor_id' => 2,
4183 'total_donation_count_compare_to' => 3,
4184 'total_donation_amount_compare_to' => $random_price_3,
4185 'user_id' => 0,
4186 'donor_email' => '[email protected]',
4187 'donor_first_name' => 'Sarah',
4188 'donor_last_name' => 'Smith',
4189 'last_donation_amount' => $random_price_4,
4190 'last_donation_campaign_links' => array(
4191 34 => 'Help the Children Campaign',
4192 ),
4193 'total_life_amount' => $random_price_3,
4194 'total_donation_count_compare_from' => 0,
4195 'total_donation_amount_compare_from' => 0,
4196 'sample' => true,
4197 ),
4198 );
4199
4200 }
4201
4202 $data = apply_filters(
4203 'charitable_report_donation_sample_data',
4204 $data,
4205 $report_type,
4206 $data_type,
4207 $number_of_records
4208 );
4209
4210 return $data;
4211 }
4212
4213 /**
4214 * Determine if the report type is a particular type of donor report.
4215 *
4216 * @since 1.8.1
4217 *
4218 * @param string $report_type The report type.
4219 *
4220 * @return string
4221 */
4222 public function is_report_type_donor( $report_type = false ) {
4223 return ( strpos( $report_type, 'donors-' ) === 0 );
4224 }
4225
4226 /**
4227 * Checks and sees if the string is a valid date, and if so return it in Y-m-d format.
4228 *
4229 * @since 1.8.1
4230 *
4231 * @param string $date_string The date string.
4232 *
4233 * @return string
4234 */
4235 public function get_valid_date_string( $date_string = false ) {
4236
4237 // Determine if $date_string is a valid date.
4238 $parsed_date = gmdate( 'Y/m/d', strtotime( $date_string ) );
4239
4240 $date = DateTime::createFromFormat( 'Y/m/d', $parsed_date );
4241
4242 if ( $date && $date->format( 'Y/m/d' ) === $parsed_date ) {
4243 return $date->format( 'Y-m-d' );
4244 } else {
4245 return false;
4246 }
4247 }
4248
4249 /**
4250 * Display upgrade notice at the bottom on the plugin settings pages.
4251 *
4252 * @since 1.8.1
4253 * @version 1.8.8
4254 *
4255 * @param string $view Current view inside the plugin settings page.
4256 *
4257 * @return void
4258 */
4259 public function reports_cta( $view = false, $css_class = 'reports-lite-cta', $show_close_button = false ) { // phpcs:ignore
4260
4261 if ( charitable_is_pro() ) {
4262 // no need to display this cta since they have a valid license.
4263 return;
4264 }
4265
4266 if ( get_option( 'charitable_lite_reports_upgrade', false ) || apply_filters( 'charitable_lite_reports_upgrade', false ) ) {
4267 return;
4268 }
4269 ?>
4270 <?php charitable_render_global_upgrade_cta( $css_class ); ?>
4271 <?php
4272 }
4273
4274 /**
4275 * Dismiss upgrade notice at the bottom on the plugin settings pages.
4276 *
4277 * @since 1.8.1
4278 */
4279 public function reports_cta_dismiss() {
4280
4281 if ( ! charitable_current_user_can() ) {
4282 wp_send_json_error();
4283 }
4284
4285 update_option( 'charitable_lite_reports_upgrade', time() );
4286
4287 wp_send_json_success();
4288 }
4289
4290 /**
4291 * See if it's the right time to load scripts.
4292 *
4293 * @since 1.8.1
4294 *
4295 * @return void
4296 */
4297 public function maybe_load_scripts() {
4298
4299 $screen = get_current_screen();
4300
4301 // Add Javascript vars at the footer of the WordPress admin, at the dashboard and the overview tab of reporting.
4302 if ( ! is_null( $screen ) && ( $screen->id === 'charitable_page_charitable-reports' && ( empty( $_GET['tab'] ) || ( ! empty( $_GET['tab'] ) && charitable_reports_allow_tab_load_scripts( strtolower( $_GET['tab'] ) ) ) ) ) ) { // phpcs:ignore
4303
4304 // Specific styles for the "overview" tab.
4305 add_action( 'admin_footer', [ $this, 'report_vars' ], 100 );
4306 }
4307 }
4308
4309 /**
4310 * See if it's the right time to load cta.
4311 *
4312 * @since 1.8.1
4313 *
4314 * @return void
4315 */
4316 public function maybe_add_reports_cta() {
4317 add_action( 'charitable_report_before_donor_table', [ $this, 'reports_cta' ] );
4318 add_action( 'charitable_report_before_activity_table', [ $this, 'reports_cta' ] );
4319 add_action( 'charitable_report_before_advanced_table', [ $this, 'reports_cta' ] );
4320 }
4321
4322 /**
4323 * Returns and/or create the single instance of this class.
4324 *
4325 * @since 1.8.1
4326 *
4327 * @return Charitable_Reports
4328 */
4329 public static function get_instance() {
4330 if ( is_null( self::$instance ) ) {
4331 self::$instance = new self();
4332 }
4333
4334 return self::$instance;
4335 }
4336 }
4337
4338 endif;
4339