PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11
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.11, at includes/admin/reports/class-charitable-reports.php

4,245 lines 149.7 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 foreach ( $days as $day_unformatted => $day_label ) {
1448
1449 $donation_today_completed = $campaign_donations_table->get_donations_summary_by_period( gmdate( 'Y-m-d', strtotime( $day_unformatted ) ), array( 'charitable-completed', 'charitable-refunded' ) );
1450 $donation_today_refunded = $campaign_donations_table->get_donations_summary_by_period( gmdate( 'Y-m-d', strtotime( $day_unformatted ) ), $refunded_statuses );
1451
1452 $donation_day = gmdate( 'Y-m-d', strtotime( $day_unformatted ) );
1453 $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day_unformatted ) );
1454 $donations_by_day[ $donation_day ]['amount'] = $donation_today_completed->amount;
1455 $donations_by_day[ $donation_day ]['donors'] = 0;
1456 $donations_by_day[ $donation_day ]['refunds'] = $donation_today_refunded->amount;
1457 $donations_by_day[ $donation_day ]['net'] = 0;
1458 }
1459
1460 // Go through the array and group the amounts and number of donors by day.
1461 foreach ( $this->donations as $donation ) {
1462
1463 $donation_day = gmdate( 'Y-m-d', strtotime( $donation->post_date ) );
1464
1465 if ( ! empty( $donation->email ) ) {
1466 if ( isset( $unique_donors[ $donation_day ][ $donation->email ] ) ) {
1467 $unique_donors[ $donation_day ][ $donation->email ] = $unique_donors[ $donation_day ][ $donation->email ] + 1;
1468 } else {
1469 $unique_donors[ $donation_day ][ $donation->email ] = 1;
1470 }
1471 }
1472
1473 if ( isset( $donations_by_day[ $donation_day ] ) ) {
1474 $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $donation->post_date ) );
1475 // $donations_by_day[ $donation_day ]['refunds'] += in_array( $donation->donation_status, $refunded_statuses, true ) ? $donation->total_amount : 0;
1476 } else {
1477 $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $donation->post_date ) );
1478 // $donations_by_day[ $donation_day ]['refunds'] = in_array( $donation->donation_status, $refunded_statuses, true ) ? $donation->total_amount : 0;
1479 }
1480 }
1481
1482 // 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.
1483 foreach ( $donations_by_day as $day => $data ) {
1484
1485 $donations_by_day[ $day ]['net'] = $this->get_net( $data['amount'], $data['refunds'] );
1486 $donations_by_day[ $day ]['donors'] = ! empty( $unique_donors[ $day ] ) ? count( $unique_donors[ $day ] ) : 0;
1487
1488 if ( $format_money ) :
1489 $donations_by_day[ $day ]['amount'] = $this->charitable_reports_format_money( $data['amount'] );
1490 $donations_by_day[ $day ]['refunds'] = $this->charitable_reports_format_money( $data['refunds'] );
1491 $donations_by_day[ $day ]['net'] = $this->charitable_reports_format_money( $donations_by_day[ $day ]['net'] );
1492 endif;
1493 }
1494
1495 // reverse array.
1496 $donations_by_day = array_reverse( $donations_by_day );
1497
1498 return $donations_by_day;
1499 }
1500
1501 /**
1502 * Get the total amount of refunds from the stored activities.
1503 *
1504 * @since 1.8.1
1505 *
1506 * @param int $campaign_id The campaign ID.
1507 * @param int $limit The limit of activities to get.
1508 *
1509 * @return float
1510 */
1511 public function get_activity_refund_total( $campaign_id = false, $limit = false ) {
1512
1513 if ( $this->is_activities_empty() ) {
1514 $this->activities = $this->get_activity( $campaign_id, $limit );
1515 }
1516
1517 if ( $this->is_activities_empty() ) {
1518 return 0;
1519 }
1520
1521 $refunded_total = 0;
1522
1523 foreach ( $this->activities as $activity ) {
1524 if ( 'refunded' === $activity->status ) {
1525 $refunded_total += $activity->amount;
1526 }
1527 }
1528
1529 return $refunded_total;
1530 }
1531
1532 /**
1533 * Get the total count of refunds from the stored activities.
1534 *
1535 * @since 1.8.1
1536 *
1537 * @param int $campaign_id The campaign ID.
1538 * @param int $limit The limit of activities to get.
1539 *
1540 * @return int
1541 */
1542 public function get_activity_refund_count( $campaign_id = false, $limit = false ) {
1543
1544 if ( $this->is_activities_empty() ) {
1545 $this->activities = $this->get_activity( $campaign_id, $limit );
1546 }
1547
1548 if ( $this->is_activities_empty() ) {
1549 return 0;
1550 }
1551
1552 $refunded_count = 0;
1553
1554 foreach ( $this->activities as $activity ) {
1555 if ( 'refunded' === $activity->status ) {
1556 ++$refunded_count;
1557 }
1558 }
1559
1560 return $refunded_count;
1561 }
1562
1563 /**
1564 * Get activity for the report.
1565 *
1566 * @since 1.8.1
1567 *
1568 * @param array $args The array of args.
1569 *
1570 * @return array
1571 */
1572 public function get_activity( $args = array() ) {
1573
1574 $defaults = array(
1575 'activity_filter_types' => array(),
1576 'activity_action_types' => array(),
1577 'start_date' => false,
1578 'end_date' => false,
1579 'status' => false,
1580 'campaign_id' => false,
1581 'category_id' => false,
1582 'limit' => false,
1583 'offset' => false,
1584 'ppage' => false,
1585 );
1586
1587 $args = wp_parse_args( $args, $defaults );
1588
1589 // Extract individual variables from args array.
1590 $activity_filter_types = $args['activity_filter_types'] ?? array();
1591 $activity_action_types = $args['activity_action_types'] ?? array();
1592 $start_date = $args['start_date'] ?? false;
1593 $end_date = $args['end_date'] ?? false;
1594 $status = $args['status'] ?? false;
1595 $campaign_id = $args['campaign_id'] ?? false;
1596 $category_id = $args['category_id'] ?? false;
1597 $limit = $args['limit'] ?? false;
1598 $offset = $args['offset'] ?? false;
1599 $ppage = $args['ppage'] ?? false;
1600
1601 if ( ! class_exists( 'Charitable_Admin_Activities' ) || ! class_exists( 'Charitable_Donation_Activities_DB' ) || ! class_exists( 'Charitable_Campaign_Activities_DB' ) ) {
1602 return;
1603 }
1604
1605 $charitable_donation_activities_db = Charitable_Donation_Activities_DB::get_instance();
1606
1607 // check to see if db tables exist.
1608 if ( ! $charitable_donation_activities_db->table_exists() ) {
1609 return;
1610 }
1611
1612 $charitable_campaign_activities_db = Charitable_Campaign_Activities_DB::get_instance();
1613
1614 // check to see if db tables exist.
1615 if ( ! $charitable_campaign_activities_db->table_exists() ) {
1616 return;
1617 }
1618
1619 // If not passed, grab from the local vars in the class.
1620 $campaign_id = $campaign_id ? $campaign_id : $this->campaign_id;
1621 $category_id = absint( $category_id ) > 0 ? $category_id : $this->category_id;
1622
1623 $admin_activities = new Charitable_Admin_Activities();
1624
1625 // Get the "raw" activities data.
1626 $activities = $admin_activities->get_activity(
1627 array(
1628 'activity_filter_types' => $activity_filter_types,
1629 'activity_action_types' => $activity_action_types,
1630 'campaign_id' => $campaign_id,
1631 'category_id' => $category_id,
1632 'start_date' => $this->start_date,
1633 'end_date' => $this->end_date,
1634 'limit' => $limit,
1635 'offset' => $offset,
1636 )
1637 );
1638
1639 // Get the more "formatted" (with additional data) reports data.
1640 $report_activities = $admin_activities->get_activity_report_data( $activities );
1641
1642 return $report_activities;
1643 }
1644
1645 /**
1646 * Generate Pagination HTML
1647 *
1648 * @since 1.8.1
1649 *
1650 * @param int $total_pages The total number of pages.
1651 * @param int $current_page The current page.
1652 * @param int $total_records Total number of records.
1653 * @param int $offset The offset.
1654 * @param int $limit The limit.
1655 * @param array $arr_params The params for the pagination links, which might depend on report type.
1656 * @param int $dot_threshold The dot threshold.
1657 *
1658 * @return void
1659 */
1660 public function show_pagination( $total_pages = 1, $current_page = 1, $total_records = false, $offset = false, $limit = 10, $arr_params = array(), $dot_threshold = false ) {
1661
1662 // Only display if pro.
1663 if ( ! charitable_is_pro() ) {
1664 return;
1665 }
1666
1667 if ( false === $dot_threshold ) {
1668 $dot_threshold = $this->pagination_dot_threshold;
1669 }
1670
1671 $total_pages = intval( $total_pages );
1672
1673 echo '<nav class="charitable-reports-pagination-nav">';
1674
1675 if ( $total_records ) :
1676 // translators: %d: total number of records.
1677 echo '<div class="page-item total-count"><span class="page-total">' . sprintf( esc_html__( 'Total: %d', 'charitable' ), intval( $total_records ) ) . '</span></div>';
1678 endif;
1679
1680 // If there are no pages, don't display anything further.
1681 if ( ! $total_pages || $total_pages <= 1 ) {
1682 echo '</nav>';
1683 return;
1684 }
1685
1686 echo '<ul>';
1687
1688 if ( intval( $total_pages ) > intval( $dot_threshold ) ) {
1689 if ( intval( $current_page ) === 1 ) {
1690 echo '<li class="page-item disabled"><span class="page-link">‹</span></li>';
1691 echo '<li class="page-item active" aria-current="page"><span class="page-link">1</span></li>';
1692 } else {
1693 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( intval( $current_page ) - 1, $arr_params ) ) . '">‹</a></li>';
1694 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( 1 ) ) . '">1</a></li>';
1695 }
1696
1697 if ( $total_pages - $current_page > 3 ) {
1698 if ( intval( $current_page ) > 4 ) {
1699 echo '<li class="page-item"><span class="page-link">...</span></li>';
1700 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>';
1701 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . ( intval( $current_page ) ) . '</span></li>';
1702 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>';
1703 } else {
1704 for ( $page_no = 2; $page_no <= 5; $page_no++ ) {
1705 if ( intval( $current_page ) === $page_no ) {
1706 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . ( intval( $page_no ) ) . '</span></li>';
1707 } else {
1708 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>';
1709 }
1710 }
1711 }
1712 }
1713
1714 if ( $total_pages - $current_page < 4 ) {
1715 echo '<li class="page-item"><span class="page-link">...</span></li>';
1716 for ( $page_no = $total_pages - 4; $page_no <= $total_pages - 1; $page_no++ ) {
1717 if ( intval( $current_page ) === $page_no ) {
1718 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . intval( $page_no ) . '</span></li>';
1719 } else {
1720 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>';
1721 }
1722 }
1723 } else {
1724 echo '<li class="page-item"><span class="page-link">...</span></li>';
1725 }
1726
1727 if ( intval( $current_page ) === intval( $total_pages ) ) {
1728 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . ( intval( $total_pages ) ) . '</span></li>';
1729 echo '<li class="page-item disabled"><span class="page-link">›</span></li>';
1730 } else {
1731 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>';
1732 echo '<li class="page-item"><a class="page-link" href="' . esc_url( $this->get_pagination_url( intval( $current_page ) + 1, $arr_params ) ) . '">›</a></li>';
1733 }
1734 } else {
1735 if ( intval( $current_page ) === 1 ) {
1736 echo '<li class="page-item disabled"><span class="page-link">‹</span></li>';
1737 } else {
1738 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
1739 }
1740 for ( $page_no = 1; $page_no <= $total_pages; $page_no++ ) {
1741 if ( intval( $current_page ) === $page_no ) {
1742 echo '<li class="page-item active" aria-current="page"><span class="page-link">' . intval( $page_no ) . '</span></li>';
1743 } else {
1744 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
1745 }
1746 }
1747 if ( intval( $current_page ) === intval( $total_pages ) ) {
1748 echo '<li class="page-item disabled"><span class="page-link">›</span></li>';
1749 } else {
1750 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
1751 }
1752 }
1753
1754 echo '</ul></nav>';
1755 }
1756
1757 /**
1758 * Get link for the charitable pagination
1759 *
1760 * @since 1.8.1.3
1761 *
1762 * @param string $total_pages Current page number.
1763 * @param string $current_page Current page.
1764 * @param string $total_records Total number of records.
1765 * @param string $offset The offset.
1766 * @param string $ppage The ppage.
1767 * @param string $limit The limit.
1768 * @param array $arr_params Params to build the attributes.
1769 *
1770 * @return string
1771 */
1772 public function get_pagination_data_attributes( $total_pages = '', $current_page = '', $total_records = 0, $offset = 0, $ppage = 1, $limit = 10, $arr_params = array() ) {
1773
1774 $data_attributes = array(
1775 'total-pages' => $total_pages,
1776 'current-page' => $current_page,
1777 'total-records' => $total_records,
1778 // 'offset' => $offset,
1779 'ppage' => $ppage, // a page in the results.
1780 'limit' => $limit, // the limit of items per page.
1781 );
1782
1783 if ( ! empty( $arr_params ) ) {
1784 foreach ( $arr_params as $key => $value ) {
1785 $data_attributes[ sanitize_title( $key ) ] = $value;
1786 }
1787 }
1788
1789 // create a string of data attributes for an HTML element based on $data_attributes.
1790 $data_attributes_string = '';
1791 foreach ( $data_attributes as $key => $value ) {
1792 $data_attributes_string .= ' data-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"';
1793 }
1794
1795 return $data_attributes_string;
1796 }
1797
1798 /**
1799 * Get link for the charitable pagination
1800 *
1801 * @since 1.8.1
1802 *
1803 * @param string $page_number Current page number.
1804 * @param array $arr_params Params to build the URL.
1805 *
1806 * @return array
1807 */
1808 public function get_pagination_url( $page_number, $arr_params = array() ) {
1809
1810 $arr_params['ppage'] = $page_number;
1811
1812 return add_query_arg( $arr_params, admin_url( 'admin.php' ) );
1813 }
1814
1815 /**
1816 * Generate the HTML for the pagination.
1817 *
1818 * @since 1.8.1
1819 *
1820 * @param int $limit The limit of items per page.
1821 * @param int $current_page The (current) page number.
1822 * @param string $report_type The report type.
1823 *
1824 * @return float
1825 */
1826 public function generate_pagination_html( $limit = 10, $current_page = 1, $report_type = 'donors-top' ) {
1827
1828 $donors = (array) $this->donors;
1829
1830 $limit = charitable_reports_is_ajax() && isset( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? intval( $_POST['limit'] ) : $limit; // phpcs:ignore
1831 $limit = false !== $limit && is_numeric( $limit ) ? $limit : charitable_reports_get_pagination_per_page();
1832
1833 $current_page = charitable_reports_is_ajax() && isset( $_POST['ppage'] ) && is_numeric( $_POST['ppage'] ) ? intval( $_POST['ppage'] ) : $current_page; // phpcs:ignore
1834 $current_page = is_numeric( $current_page ) ? $current_page : 1;
1835
1836 $pagination = array(
1837 'length' => isset( $_GET['limit'] ) ? (int) $_GET['limit'] : $limit, // phpcs:ignore
1838 'total' => 0 !== $this->donors_total ? $this->donors_total : count( $donors ),
1839 'currentPage' => isset( $_GET['ppage'] ) ? (int) $_GET['ppage'] : $current_page, // phpcs:ignore
1840 );
1841
1842 $pagination['total_pages'] = ceil( $pagination['total'] / $pagination['length'] );
1843 $pagination['offset'] = ( $pagination['currentPage'] * $pagination['length'] ) - $pagination['length'];
1844
1845 // The params for the pagination links, which might depend on report type.
1846 $arr_params = array(
1847 'page' => 'charitable-reports',
1848 'tab' => 'donors',
1849 'report-type' => isset( $_GET['report_type'] ) ? sanitize_text_field( wp_unslash( $_GET['report_type'] ) ) : $report_type, // phpcs:ignore
1850 );
1851
1852 ob_start();
1853
1854 $this->show_pagination( $pagination['total_pages'], $pagination['currentPage'], $pagination['total'], $pagination['offset'], $pagination['length'], $arr_params );
1855
1856 return ob_get_clean();
1857 }
1858
1859 /**
1860 * Get the activity icon url from a status string.
1861 *
1862 * @since 1.8.1
1863 *
1864 * @param string $icon The icon name.
1865 *
1866 * @return string
1867 */
1868 public function get_activity_icon_url( $icon = false ) {
1869
1870 if ( false === $icon || '' === $icon ) {
1871 return false;
1872 }
1873
1874 return charitable()->get_path( 'directory', false ) . 'assets/images/icons/activities/' . $icon . '.png';
1875 }
1876
1877 /**
1878 * Get the top donors for the dedicated donor report.
1879 *
1880 * @since 1.8.1
1881 *
1882 * @param int $limit The limit of donors to get.
1883 * @param int $offset The offset of donors to get.
1884 *
1885 * @return string
1886 */
1887 public function get_top_donors( $limit = false, $offset = false ) { // phpcs:ignore
1888 if ( empty( $this->donors ) ) {
1889 return;
1890 }
1891
1892 return $this->donors;
1893 }
1894
1895 /**
1896 * Get the top donors for the dedicated donor report.
1897 *
1898 * @since 1.8.1
1899 *
1900 * @param int $limit The limit of donors to get.
1901 *
1902 * @return string
1903 */
1904 public function get_recurring_donors( $limit = false ) { // phpcs:ignore
1905 if ( empty( $this->donors ) ) {
1906 return;
1907 }
1908
1909 return $this->donors;
1910 }
1911
1912 /**
1913 * Get the first time donors for the dedicated donor report.
1914 *
1915 * @since 1.8.1
1916 *
1917 * @param int $limit The limit of donors to get.
1918 *
1919 * @return string
1920 */
1921 public function get_first_time_donors( $limit = false ) { // phpcs:ignore
1922 if ( empty( $this->donors ) ) {
1923 return;
1924 }
1925
1926 return $this->donors;
1927 }
1928
1929 /**
1930 * Get the donors for the dedicated donor report.
1931 *
1932 * @since 1.8.1
1933 *
1934 * @param string $report_type The report type.
1935 * @param array $args The array of args.
1936 *
1937 * @return array|bool
1938 */
1939 public function get_donor_report_by_type( $report_type = false, $args = array() ) {
1940
1941 $defaults = array(
1942 'limit' => false,
1943 'offset' => false,
1944 'ppage' => false,
1945 );
1946
1947 $args = wp_parse_args( $args, $defaults );
1948
1949 // Extract individual variables from args array.
1950 $limit = $args['limit'] ?? false;
1951 $offset = $args['offset'] ?? false;
1952 $ppage = $args['ppage'] ?? false;
1953
1954 if ( ! $offset && $ppage && $limit ) {
1955 $offset = ( $ppage - 1 ) * $limit;
1956 }
1957
1958 switch ( $report_type ) {
1959 case 'donors-top':
1960 return ( $this->get_top_donors( $limit, $offset ) );
1961 break; // phpcs:ignore
1962 case 'donors-recurring':
1963 return ( $this->get_recurring_donors( $limit, $offset ) );
1964 break; // phpcs:ignore
1965 case 'donors-first-time':
1966 return ( $this->get_first_time_donors( $limit, $offset ) );
1967 break; // phpcs:ignore
1968
1969 default:
1970 return false;
1971 break; // phpcs:ignore
1972 }
1973 }
1974
1975 /**
1976 * Return the HTML of the filter select dropdown.
1977 *
1978 * @since 1.8.1
1979 *
1980 * @param string $selected_value The selected value.
1981 *
1982 * @return string
1983 */
1984 public function get_donor_report_type_dropdown( $selected_value = false ) {
1985
1986 $options_values = apply_filters(
1987 'charitable_dashboard_donor_report_type_filter_values',
1988 array(
1989 'donors-top' => __( 'Top Donors', 'charitable' ),
1990 'donors-first-time' => __( 'First Time Donors', 'charitable' ),
1991 'donors-recurring' => __( 'Recurring Donors', 'charitable' ),
1992 )
1993 );
1994
1995 ob_start();
1996
1997 ?>
1998
1999 <select name="action" id="report-donor-type-filter">
2000 <?php foreach ( $options_values as $value => $label ) : ?>
2001 <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_value, $value ); ?>><?php echo esc_html( $label ); ?></option>
2002 <?php endforeach; ?>
2003 </select>
2004
2005 <?php
2006
2007 return ob_get_clean();
2008 }
2009
2010 /**
2011 * Get the top donors for the overview report.
2012 *
2013 * @since 1.8.1
2014 *
2015 * @param int $limit The limit of donors to get.
2016 *
2017 * @return string
2018 */
2019 public function get_top_donors_overview( $limit = false ) { // phpcs:ignore
2020 if ( empty( $this->donations ) ) {
2021 return;
2022 }
2023
2024 $donations_by_donors = (array) $this->donations;
2025 $unique_donors = array();
2026
2027 // sort multi-dimensional array by 'amount'.
2028 usort(
2029 $donations_by_donors,
2030 function ( $a, $b ) {
2031 return $b->total_amount - $a->total_amount;
2032 }
2033 );
2034
2035 foreach ( $donations_by_donors as $index => $donation ) {
2036
2037 if ( empty( $donation->donor_id ) || 0 === $donation->donor_id ) {
2038 continue;
2039 }
2040
2041 $donor = charitable_get_table( 'donors' )->get_personal_data_by_donor_id( $donation->donor_id );
2042
2043 if ( empty( $donor ) ) {
2044 continue;
2045 }
2046
2047 $donor = $donor[0];
2048
2049 // combine first and last name from $donor into $donor_name, if either exist.
2050 $donor_name = ( ! empty( $donor->first_name ) || ! empty( $donor->last_name ) ? $donor->first_name . ' ' . $donor->last_name : 'Unknown' );
2051 $donor_email = ( ! empty( $donor->email ) ) ? $donor->email : '';
2052
2053 $donations_by_donors[ $index ]->donor_name = trim( $donor_name );
2054 $donations_by_donors[ $index ]->donor_email = $donor_email;
2055
2056 $donations_by_donors[ $index ]->donor_avatar_url = isset( $donation->user_id ) && $donation->user_id !== 0 ? get_avatar_url( $donation->user_id, 32 ) : 0;
2057 $donations_by_donors[ $index ]->donation_type = charitable_get_donation( $donation->donation_id )->get( 'donation_type' );
2058 $donations_by_donors[ $index ]->donation_parent_id = $donation->donation_parent_id;
2059
2060 if ( ! isset( $unique_donors[ $donation->donor_id ] ) ) {
2061 $unique_donors[ $donation->donor_id ]['amount'] = $donation->total_amount;
2062 $unique_donors[ $donation->donor_id ]['count'] = 1;
2063 } else {
2064 $unique_donors[ $donation->donor_id ]['amount'] += $donation->total_amount;
2065 $unique_donors[ $donation->donor_id ]['count'] = $unique_donors[ $donation->donor_id ]['count'] + 1;
2066 }
2067 }
2068
2069 // go back and add the total donation amount to the $donations_by_donors array.
2070 foreach ( $donations_by_donors as $index => $donation ) {
2071
2072 if ( ! empty( $unique_donors[ $donation->donor_id ] ) ) {
2073 $donations_by_donors[ $index ]->total_donation_amount = $unique_donors[ $donation->donor_id ]['amount'];
2074 $donations_by_donors[ $index ]->total_donation_count = $unique_donors[ $donation->donor_id ]['count'];
2075 }
2076 }
2077
2078 // sort multi-dimensional array by 'total_donation_amount'.
2079 // @since 1.8.8.6 - Fixed undefined property warning by checking if property exists before accessing.
2080 usort(
2081 $donations_by_donors,
2082 function ( $a, $b ) {
2083 // @since 1.8.8.6 - Check if total_donation_amount exists, default to 0 if not set.
2084 $a_amount = isset( $a->total_donation_amount ) ? $a->total_donation_amount : 0;
2085 $b_amount = isset( $b->total_donation_amount ) ? $b->total_donation_amount : 0;
2086 return $b_amount - $a_amount;
2087 }
2088 );
2089
2090 return $donations_by_donors;
2091 }
2092
2093 /**
2094 * Get the name of the cache value (or key) for the report we use for the database.
2095 *
2096 * @since 1.8.1
2097 *
2098 * @param string $report_type The report type.
2099 * @param string $start_date The start date.
2100 * @param string $end_date The end date.
2101 * @param string $post_status The post status.
2102 * @param string $campaign_id The campaign ID.
2103 * @param string $category_id The category ID.
2104 *
2105 * @return string
2106 */
2107 public function get_cache_key( $report_type = 'overview', $start_date = false, $end_date = false, $post_status = false, $campaign_id = false, $category_id = false ) {
2108
2109 $cache_key = sanitize_title( $start_date . $end_date . str_replace( 'charitable', '', $post_status ) . $campaign_id . $category_id );
2110
2111 // remove all dashes from the cache key.
2112 $cache_key = 'wpch-report-' . $report_type . '-' . str_replace( '-', '', $cache_key );
2113
2114 return $cache_key;
2115 }
2116
2117 /**
2118 * Get the cached report.
2119 *
2120 * @since 1.8.1
2121 *
2122 * @param string $report_type The report type.
2123 * @param string $start_date The start date.
2124 * @param string $end_date The end date.
2125 * @param string $post_status The post status.
2126 * @param string $campaign_id The campaign ID.
2127 * @param string $category_id The category ID.
2128 *
2129 * @return array|bool
2130 */
2131 public function get_cached_report( $report_type = 'overview', $start_date = false, $end_date = false, $post_status = false, $campaign_id = false, $category_id = false ) {
2132
2133 if ( ! $this->maybe_cache_report( true ) ) {
2134 return false;
2135 }
2136
2137 $cache_key = $this->get_cache_key( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id );
2138
2139 $data = get_transient( $cache_key );
2140
2141 if ( false === $data ) {
2142 return false;
2143 }
2144
2145 return $data;
2146 }
2147
2148 /**
2149 * Set the cached report.
2150 *
2151 * @since 1.8.1
2152 *
2153 * @param string $report_type The report type.
2154 * @param string $start_date The start date.
2155 * @param string $end_date The end date.
2156 * @param string $post_status The post status.
2157 * @param string $campaign_id The campaign ID.
2158 * @param string $category_id The category ID.
2159 * @param array $data The data to cache.
2160 *
2161 * @return bool
2162 */
2163 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() ) {
2164
2165 if ( ! $this->maybe_cache_report( true ) ) {
2166 return false;
2167 }
2168
2169 $cache_key = $this->get_cache_key( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id );
2170
2171 $was_set = set_transient( $cache_key, $data, MINUTE_IN_SECONDS );
2172
2173 return $was_set;
2174 }
2175
2176 /**
2177 * Check if the report should be cached.
2178 *
2179 * @since 1.8.1
2180 *
2181 * @param bool $force_cache Whether to force the cache.
2182 *
2183 * @return bool
2184 */
2185 public function maybe_cache_report( $force_cache = false ) {
2186
2187 // Check the globals to see if caching is enabled specifically for reports.
2188 if ( $force_cache ) {
2189 return true;
2190 }
2191 if ( charitable_is_debug() ) {
2192 return false;
2193 }
2194 if ( defined( 'CHARITABLE_REPORTS_NO_CACHE' ) && CHARITABLE_REPORTS_NO_CACHE ) { // phpcs:ignore
2195 return false;
2196 }
2197
2198 return true;
2199 }
2200
2201 /**
2202 * This accepts a report type and date and attemps to create a report that has multiple data points
2203 *
2204 * @param string $report_type The type of report to create.
2205 * @param array $args The array of args.
2206 *
2207 * @since 1.8.1
2208 *
2209 * @return array
2210 */
2211 public function get_data( $report_type = 'overview', $args = array() ) {
2212
2213 global $wpdb;
2214
2215 $defaults = array(
2216 'report_type' => 'overview',
2217 'start_date' => false,
2218 'end_date' => false,
2219 'post_status' => 'charitable-completed',
2220 'campaign_id' => false,
2221 'category_id' => false,
2222 );
2223
2224 $args = wp_parse_args( $args, $defaults );
2225
2226 // Extract individual variables from args array.
2227 $report_type = $args['report_type'] ?? 'overview';
2228 $start_date = $args['start_date'] ?? false;
2229 $end_date = $args['end_date'] ?? false;
2230 $post_status = $args['post_status'] ?? 'charitable-completed';
2231 $campaign_id = $args['campaign_id'] ?? false;
2232 $category_id = $args['category_id'] ?? false;
2233
2234 // Check the post_status and set it to the default if it's not a valid status.
2235 if ( ! in_array( $post_status, array( 'charitable-completed', 'charitable-pending', 'charitable-failed', 'charitable-cancelled', 'charitable-refunded' ), true ) ) {
2236 $post_status = 'charitable-completed';
2237 }
2238
2239 $this->donations = ! charitable_is_pro() && $report_type !== 'overview' ? $this->get_data_sample( $report_type ) : false;
2240
2241 // Determine if this particular report has been cached recently.
2242 $this->donations = false; // $this->get_cached_report( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id );
2243
2244 if ( false === $this->donations ) {
2245
2246 // No cache? Let's run the data.
2247
2248 $where_sql = array();
2249 $where_sql[] = 'WHERE 1=1';
2250 $where_sql[] = ( intval( $campaign_id ) > 0 ) ? 'cd.campaign_id = ' . intval( $campaign_id ) : false;
2251 $where_sql[] = 'p.post_type = "%s"';
2252 $where_sql[] = 'p.post_status = "' . $post_status . '"';
2253 $where_sql[] = ( $start_date ) ? "p.post_date >= '" . $start_date . " 00:00:00'" : false;
2254 $where_sql[] = ( $end_date ) ? "p.post_date <= '" . $end_date . " 23:59:59'" : false;
2255 $where_sql[] = ( intval( $category_id ) > 0 ) ? 'tr.term_taxonomy_id = ' . intval( $category_id ) : false;
2256 $where_sql[] = 'pm1.meta_key = "donation_gateway"';
2257 // remove all empty values from the array.
2258 $where_sql = array_filter( $where_sql );
2259 $where_args = implode( ' AND ', $where_sql );
2260
2261 $left_join = array();
2262 $left_join[] = $wpdb->prefix . 'charitable_campaign_donations cd ON p.ID = cd.donation_id';
2263 $left_join[] = $wpdb->prefix . 'charitable_donors cdonors ON cd.donor_id = cdonors.donor_id';
2264 $left_join[] = $wpdb->prefix . 'postmeta pm1 ON p.ID = pm1.post_id';
2265 $left_join[] = ( intval( $category_id ) > 0 ) ? $wpdb->prefix . 'term_relationships tr ON tr.object_id = campaign_id' : false;
2266 // remove all empty values from the array.
2267 $left_join = array_filter( $left_join );
2268 $left_join_args = 'LEFT JOIN ' . implode( ' LEFT JOIN ', $left_join );
2269
2270 $sql = "SELECT SUM(cd.amount) AS total_amount,
2271 cd.donation_id AS donation_id,
2272 cd.campaign_id AS campaign_id,
2273 cd.donor_id AS donor_id,
2274 cd.amount AS amount,
2275 pm1.meta_value AS payment_gateway,
2276 p.post_date AS post_date,
2277 p.post_date_gmt AS post_date_gmt,
2278 p.post_status AS donation_status,
2279 p.post_parent AS donation_parent_id,
2280 cdonors.first_name AS first_name,
2281 cdonors.last_name AS last_name,
2282 cdonors.email AS email
2283 FROM $wpdb->posts p
2284 " . $left_join_args . '
2285 ' . $where_args . '
2286 GROUP BY p.ID';
2287
2288 // get all donations (posts with a post type of donation) between a start date and an end date.
2289 $this->donations = $wpdb->get_results( // phpcs:ignore
2290 $wpdb->prepare(
2291 $sql, // phpcs:ignore
2292 'donation'
2293 )
2294 );
2295
2296 // Save this to the cache.
2297 // $this->set_cached_report( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id, $this->donations );.
2298
2299 }
2300
2301 $this->init_axis_with_donations( $this->donations, $start_date, $end_date );
2302
2303 return $this->donations;
2304 }
2305
2306 /**
2307 * This accepts a report type and date and attemps to create a report specific to a donor type report.
2308 *
2309 * @since 1.8.1
2310 *
2311 * @param string $report_type The type of report to create.
2312 * @param string $start_date The start date.
2313 * @param string $end_date The end date.
2314 * @param string $campaign_id The campaign ID.
2315 * @param string $category_id The category ID.
2316 *
2317 * @return array
2318 */
2319 public function get_donors_data( $report_type = 'donors-top', $start_date = false, $end_date = false, $campaign_id = false, $category_id = false ) { // phpcs:ignore
2320
2321 global $wpdb;
2322
2323 // Determine if this particular report gets sample data, or if not if it's has been cached recently.
2324 $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 );
2325
2326 if ( false === $this->donors ) {
2327
2328 // No cache? Let's run the data.
2329
2330 $having_sql = '';
2331 $order_by = '';
2332
2333 switch ( $report_type ) {
2334 case 'donors-recurring':
2335 $having_sql = 'HAVING total_count_donations > 1';
2336 $order_by = 'ORDER BY %s DESC';
2337 $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_recurring_sql_limit', 100 );
2338
2339 $sql = "SELECT SUM( ccd.amount ) AS total_amount,
2340 AVG( ccd.amount ) AS average,
2341 COUNT( ccd.donation_id ) AS total_count_donations,
2342 COUNT( DISTINCT( ccd.campaign_id ) ) AS total_count_campaigns,
2343 last_donation.post_date AS last_donation_date,
2344 last_campaign.post_title AS last_campaign_title,
2345 last_campaign.ID AS last_campaign_id,
2346 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
2347
2348 FROM {$wpdb->prefix}charitable_donors cd
2349
2350 JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id
2351 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
2352 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
2353
2354 GROUP BY cd.donor_id
2355
2356 {$having_sql}
2357
2358 {$order_by}
2359
2360 {$limit}
2361
2362 ";
2363
2364 $this->donors = $wpdb->get_results( // phpcs:ignore
2365 $wpdb->prepare(
2366 $sql, // phpcs:ignore
2367 'total_amount'
2368 )
2369 );
2370
2371 break;
2372
2373 case 'donors-first-time':
2374 $having_sql = 'HAVING total_count_donations = 1';
2375 $order_by = 'ORDER BY %s DESC'; // not currently being used.
2376 $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_first_time_sql_limit', 100 );
2377
2378 $sql = "SELECT ccd.amount AS total_amount,
2379 COUNT( ccd.donation_id ) AS total_count_donations,
2380 last_donation.post_date AS last_donation_date,
2381 last_campaign.post_title AS last_campaign_title,
2382 last_campaign.ID AS last_campaign_id,
2383 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
2384
2385 FROM {$wpdb->prefix}charitable_donors cd
2386
2387 JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id
2388 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
2389 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
2390
2391 GROUP BY cd.donor_id
2392
2393 {$having_sql}
2394
2395 ORDER BY last_donation_date DESC
2396
2397 {$limit}
2398
2399 ";
2400
2401 $this->donors = $wpdb->get_results( // phpcs:ignore
2402 $wpdb->prepare(
2403 $sql // phpcs:ignore
2404 )
2405 );
2406
2407 break;
2408
2409 default:
2410 $order_by = 'ORDER BY %s DESC';
2411 $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_default_sql_limit', 100, $report_type );
2412
2413 $sql = "SELECT SUM( ccd.amount ) AS total_amount,
2414 AVG( ccd.amount ) AS average,
2415 COUNT( ccd.donation_id ) AS total_count_donations,
2416 COUNT( DISTINCT( ccd.campaign_id ) ) AS total_count_campaigns,
2417 last_donation.post_date AS last_donation_date,
2418 last_campaign.post_title AS last_campaign_title,
2419 last_campaign.ID AS last_campaign_id,
2420 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
2421
2422 FROM {$wpdb->prefix}charitable_donors cd
2423
2424 JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id
2425 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
2426 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
2427
2428 GROUP BY cd.donor_id
2429
2430 {$having_sql}
2431
2432 {$order_by}
2433
2434 {$limit}
2435
2436 ";
2437
2438 $this->donors = $wpdb->get_results( // phpcs:ignore
2439 $wpdb->prepare(
2440 $sql, // phpcs:ignore
2441 'total_amount'
2442 )
2443 );
2444
2445 break;
2446 }
2447
2448 // Save this to the cache.
2449 // $this->set_cached_report( $report_type, $start_date, $end_date, false, $campaign_id, $category_id, $this->donors );.
2450
2451 return $this->donors;
2452
2453 }
2454 }
2455
2456 /**
2457 * This accepts a report type and date and attemps to create a report that has multiple data points
2458 *
2459 * @since 1.8.1
2460 * @since 1.8.3 Updated w/ limit and offset sql, etc.
2461 *
2462 * @param string $report_type The type of report to create.
2463 * @param array $args The array of args.
2464 *
2465 * @return array
2466 */
2467 public function get_donors_data_array( $report_type = 'donors-top', $args = array() ) {
2468
2469 global $wpdb;
2470
2471 $defaults = array(
2472 'report_type' => 'donors-top',
2473 'start_date' => false,
2474 'end_date' => false,
2475 'campaign_id' => false,
2476 'category_id' => false,
2477 'limit' => false,
2478 'offset' => false,
2479 );
2480
2481 $args = wp_parse_args( $args, $defaults );
2482
2483 // Extract individual variables from args array.
2484 $report_type = $args['report_type'] ?? 'donors-top';
2485 $start_date = $args['start_date'] ?? false;
2486 $end_date = $args['end_date'] ?? false;
2487 $campaign_id = $args['campaign_id'] ?? false;
2488 $category_id = $args['category_id'] ?? false;
2489 $limit = $args['limit'] ?? false;
2490 $offset = $args['offset'] ?? false;
2491 $ppage = $args['ppage'] ?? false;
2492
2493 // Determine if this particular report gets sample data, or if not if it's has been cached recently.
2494 $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 );
2495
2496 if ( false === $this->donors ) {
2497
2498 // No cache? Let's run the data.
2499
2500 $having_sql = '';
2501 $order_by = '';
2502 $where = '';
2503 // generate mysql LIMIT and OFFSET.
2504 if ( ! $offset && $ppage && $limit ) {
2505 $offset = ( $ppage - 1 ) * $limit;
2506 }
2507 $limit_sql = ( $limit ) ? 'LIMIT ' . $limit : '';
2508 $offset_sql = ( $offset ) ? 'OFFSET ' . $offset : '';
2509
2510 switch ( $report_type ) {
2511 case 'donors-recurring':
2512 $having_sql = 'HAVING total_count_donations > 1';
2513 $order_by = 'ORDER BY total_amount DESC';
2514
2515 $query = "SELECT SUM( ccd2.amount ) AS total_amount,
2516 AVG( ccd2.amount ) AS average,
2517 COUNT( ccd2.donation_id ) AS total_count_donations,
2518 COUNT( DISTINCT( ccd2.campaign_id ) ) AS total_count_campaigns,
2519 last_donation.post_date AS last_donation_date,
2520 last_campaign.post_title AS last_campaign_title,
2521 last_campaign.ID AS last_campaign_id,
2522 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
2523
2524 FROM {$wpdb->prefix}charitable_donors cd
2525
2526 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
2527 FROM {$wpdb->prefix}charitable_campaign_donations
2528 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
2529 ) ccd ON ccd.donor_id = cd.donor_id
2530
2531 JOIN ( SELECT donor_id, donation_id, campaign_id, amount, campaign_donation_id FROM {$wpdb->prefix}charitable_campaign_donations
2532 JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}charitable_campaign_donations.donation_ID
2533 WHERE {$wpdb->prefix}posts.post_status IN ('charitable-completed')
2534
2535 ) ccd2 ON ccd2.donor_id = cd.donor_id
2536
2537 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_donation ON ccd.donation_id = last_donation.ID
2538 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_campaign ON ccd.campaign_id = last_campaign.ID
2539
2540 {$where}
2541
2542 GROUP BY cd.donor_id
2543
2544 {$having_sql}
2545
2546 {$order_by}
2547
2548 ";
2549
2550 $this->donors = $wpdb->get_results( // phpcs:ignore
2551 $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore
2552 );
2553
2554 break;
2555
2556 case 'donors-first-time':
2557 $having_sql = 'HAVING total_count_donations = 1';
2558 $order_by = 'ORDER BY last_donation_date ASC';
2559
2560 $query = "SELECT ccd.amount AS total_amount,
2561 COUNT( ccd.donation_id ) AS total_count_donations,
2562 last_donation.post_date AS last_donation_date,
2563 last_campaign.post_title AS last_campaign_title,
2564 last_campaign.ID AS last_campaign_id,
2565 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
2566
2567 FROM {$wpdb->prefix}charitable_donors cd
2568
2569 JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id
2570 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
2571 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
2572
2573 {$where}
2574
2575 GROUP BY cd.donor_id
2576
2577 {$having_sql}
2578
2579 {$order_by}
2580
2581 ";
2582
2583 $this->donors = $wpdb->get_results( // phpcs:ignore
2584 $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore
2585 );
2586
2587 break;
2588
2589 default:
2590 $order_by = 'ORDER BY total_amount DESC';
2591
2592 $query = "SELECT SUM( ccd2.amount ) AS total_amount,
2593 AVG( ccd2.amount ) AS average,
2594 COUNT( ccd2.donation_id ) AS total_count_donations,
2595 COUNT( DISTINCT( ccd2.campaign_id ) ) AS total_count_campaigns,
2596 last_donation.post_date AS last_donation_date,
2597 last_campaign.post_title AS last_campaign_title,
2598 last_campaign.ID AS last_campaign_id,
2599 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
2600
2601 FROM {$wpdb->prefix}charitable_donors cd
2602
2603 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
2604 FROM {$wpdb->prefix}charitable_campaign_donations
2605 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
2606 ) ccd ON ccd.donor_id = cd.donor_id
2607
2608 JOIN ( SELECT donor_id, donation_id, campaign_id, amount, campaign_donation_id FROM {$wpdb->prefix}charitable_campaign_donations
2609 JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}charitable_campaign_donations.donation_ID
2610 WHERE {$wpdb->prefix}posts.post_status IN ('charitable-completed')
2611
2612 ) ccd2 ON ccd2.donor_id = cd.donor_id
2613
2614 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_donation ON ccd.donation_id = last_donation.ID
2615 INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_campaign ON ccd.campaign_id = last_campaign.ID
2616
2617 {$where}
2618
2619 GROUP BY cd.donor_id
2620
2621 {$having_sql}
2622
2623 {$order_by}";
2624
2625 $this->donors = $wpdb->get_results( // phpcs:ignore
2626 $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore
2627 );
2628
2629 break;
2630 }
2631
2632 $total_query = 'SELECT COUNT(1) FROM (' . $query . ') AS combined_table';
2633 $this->donors_total = $wpdb->get_var( $total_query ); // phpcs:ignore
2634
2635 // Save this to the cache.
2636 // $this->set_cached_report( $report_type, $start_date, $end_date, false, $campaign_id, $category_id, $this->donors );.
2637
2638 return $this->donors;
2639
2640 }
2641 }
2642
2643 /**
2644 * This accepts donations and attempts to create data for the headline chart with donation data.
2645 *
2646 * @param array $donations An array of donations.
2647 * @param string $start_date The start date.
2648 * @param string $end_date The end date.
2649 *
2650 * @since 1.8.1
2651 */
2652 public function init_axis_with_donations( $donations = array(), $start_date = false, $end_date = false ) {
2653
2654 if ( false === $donations ) {
2655 return;
2656 }
2657
2658 $date_data = array();
2659 $donation_data = array();
2660
2661 foreach ( $donations as $donation ) {
2662
2663 $donation_day = gmdate( 'M d', strtotime( $donation->post_date ) );
2664
2665 if ( ! in_array( $donation_day, $date_data, true ) ) {
2666 $date_data[] = $donation_day;
2667 }
2668
2669 if ( isset( $donation_data[ $donation_day ]['amount'] ) ) {
2670 $donation_data[ $donation_day ]['amount'] += $donation->total_amount;
2671 } else {
2672 $donation_data[ $donation_day ]['amount'] = $donation->total_amount;
2673 }
2674
2675 }
2676
2677 $this->set_date_axis( $date_data, $start_date, $end_date );
2678 $this->set_donation_axis( $donation_data, $start_date, $end_date );
2679 }
2680
2681 /**
2682 * This generates the donation axis for the headline chart.
2683 *
2684 * @param array $donation_data An array of donations.
2685 * @param string $start_date The start date.
2686 * @param string $end_date The end date.
2687 *
2688 * @since 1.8.1
2689 */
2690 public function set_donation_axis( $donation_data, $start_date, $end_date ) {
2691
2692 $this->donation_axis = array();
2693
2694 $days = $this->get_days_between_dates( $start_date, $end_date );
2695
2696 if ( ! $days || empty( $days ) ) {
2697 return;
2698 }
2699
2700 foreach ( $days as $day ) {
2701 $this->donation_axis[ $day ] = ! empty( $donation_data[ $day ]['amount'] ) ? $donation_data[ $day ]['amount'] : 0;
2702 }
2703
2704 // Remove the keys from the array but keep the order of the values.
2705 $this->donation_axis = array_values( $this->donation_axis );
2706
2707 // 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.
2708 if ( count( $days ) === 1 ) {
2709 $this->donation_axis = array_merge( array( 0 ), $this->donation_axis, array( 0 ) );
2710 }
2711
2712 return $this->donation_axis;
2713 }
2714
2715 /**
2716 * This generates the date axis for the headline chart.
2717 *
2718 * @param array $date_data An array of dates.
2719 * @param string $start_date The start date.
2720 * @param string $end_date The end date.
2721 *
2722 * @since 1.8.1
2723 */
2724 public function set_date_axis( $date_data, $start_date, $end_date ) {
2725
2726 $days = $this->get_days_between_dates( $start_date, $end_date, false );
2727
2728 // 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.
2729 if ( count( $days ) === 1 ) {
2730 $days = array_merge( array( gmdate( 'M d', strtotime( $start_date . ' -1 day' ) ) ), $days, array( gmdate( 'M d', strtotime( $end_date . ' +1 day' ) ) ) );
2731 }
2732
2733 $this->date_axis = $days;
2734
2735 return $days;
2736 }
2737
2738 /**
2739 * Util function that gets dates between two given dates.
2740 *
2741 * @param string $start_date The start date.
2742 * @param string $end_date The end date.
2743 * @param string $array_key_format The format of the array key.
2744 *
2745 * @since 1.8.1
2746 *
2747 * @return mixed
2748 */
2749 public function get_days_between_dates( $start_date, $end_date, $array_key_format = 'Y-m-d' ) { // phpcs:ignore
2750 $days = array();
2751
2752 $current_date = new DateTime( $start_date );
2753 $end_date = new DateTime( $end_date );
2754
2755 while ( $current_date <= $end_date ) {
2756 if ( $array_key_format ) :
2757 $days[ $current_date->format( $array_key_format ) ] = $current_date->format( 'M d' );
2758 else :
2759 $days[] = $current_date->format( 'M d' );
2760 endif;
2761
2762 $current_date->modify( '+1 day' );
2763 }
2764
2765 return $days;
2766 }
2767
2768 /**
2769 * This accepts a report type and date and attemps to create a refund "report".
2770 *
2771 * @since 1.8.1
2772 * @version 1.8.8.6
2773 *
2774 * @param string $campaign_id The campaign ID.
2775 * @param string $limit The limit of refunds to get.
2776 *
2777 * @return array
2778 */
2779 public function get_refunds( $campaign_id = false, $limit = false ) { // phpcs:ignore
2780
2781 global $wpdb;
2782
2783 $where_sql = array();
2784 $prepare_args = array();
2785
2786 $where_sql[] = 'p.post_status = %s';
2787 $prepare_args[] = 'charitable-refunded';
2788
2789 // add to $where_sql depending on the values of $start_date and $end_date.
2790 if ( $this->start_date ) {
2791 $where_sql[] = 'p.post_date >= %s';
2792 $prepare_args[] = $this->start_date . ' 00:00:00';
2793 }
2794 if ( $this->end_date ) {
2795 $where_sql[] = 'p.post_date <= %s';
2796 $prepare_args[] = $this->end_date . ' 23:59:59';
2797 }
2798
2799 $where_sql[] = 'pm1.meta_key = %s';
2800 $prepare_args[] = 'donation_gateway';
2801
2802 $where_clause = implode( ' AND ', $where_sql );
2803
2804 // Build prepare args with post_type first, then where clause args.
2805 $all_prepare_args = array_merge( array( 'donation' ), $prepare_args );
2806
2807 // get all donations (posts with a post type of donation) between a start date and an end date.
2808 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
2809 // $where_clause is built with placeholders (%s) that are properly processed by $wpdb->prepare().
2810 // The interpolation is safe because all values in $where_clause are placeholders, not raw values.
2811 $refunded_donations = $wpdb->get_results(
2812 $wpdb->prepare(
2813 "SELECT SUM(cd.amount) AS total_amount,
2814 COUNT(p.ID) AS total_number_of_refunds,
2815 cd.donation_id AS donation_id,
2816 cd.campaign_id AS campaign_id,
2817 cd.donor_id AS donor_id,
2818 cd.amount AS amount,
2819 pm1.meta_value AS payment_gateway,
2820 p.post_date AS post_date,
2821 p.post_date_gmt AS post_date_gmt
2822 FROM {$wpdb->posts} p
2823 LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id
2824 LEFT JOIN {$wpdb->prefix}postmeta pm1 ON p.ID = pm1.post_id
2825 WHERE p.post_type = %s AND {$where_clause}
2826 GROUP BY p.ID",
2827 ...$all_prepare_args
2828 )
2829 );
2830 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
2831
2832 $refunds_by_day = array();
2833 $refunded_donations_total_amount = 0;
2834 $refunded_donations_total_count = 0;
2835 $processed_donation_ids = array(); // Track unique donation IDs to avoid double-counting
2836
2837 $days = $this->get_days_between_dates( $this->start_date, $this->end_date );
2838
2839 // First, calculate totals from all refunded donations (regardless of day matching)
2840 // This ensures count and amount are consistent
2841 if ( ! empty( $refunded_donations ) ) {
2842 foreach ( $refunded_donations as $refunded_donation ) {
2843 // Only count each unique donation ID once
2844 $donation_id = isset( $refunded_donation->donation_id ) ? intval( $refunded_donation->donation_id ) : 0;
2845 if ( $donation_id > 0 && ! in_array( $donation_id, $processed_donation_ids, true ) ) {
2846 $refunded_donations_total_amount += floatval( $refunded_donation->amount );
2847 $refunded_donations_total_count++;
2848 $processed_donation_ids[] = $donation_id;
2849 }
2850 }
2851 }
2852
2853 // Then, organize by day for the breakdown
2854 foreach ( $days as $day ) {
2855 $donation_day = gmdate( 'Y-m-d', strtotime( $day ) );
2856 $refunded_donation_amount = 0;
2857 $refunded_donation_count = 0;
2858 $processed_day_donation_ids = array(); // Track unique donation IDs per day
2859 if ( ! empty( $refunded_donations ) ) :
2860 foreach ( $refunded_donations as $refunded_donation ) {
2861 // if the refunded donation post date is the same day as the donation_day, add it to the refunded total.
2862 $refund_date = gmdate( 'Y-m-d', strtotime( $refunded_donation->post_date_gmt ) );
2863 if ( $refund_date === $donation_day ) {
2864 $donation_id = isset( $refunded_donation->donation_id ) ? intval( $refunded_donation->donation_id ) : 0;
2865 // Only count each unique donation ID once per day
2866 if ( $donation_id > 0 && ! in_array( $donation_id, $processed_day_donation_ids, true ) ) {
2867 $refunded_donation_amount += floatval( $refunded_donation->amount );
2868 ++$refunded_donation_count;
2869 $processed_day_donation_ids[] = $donation_id;
2870 }
2871 }
2872 }
2873 endif;
2874 $refunds_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day ) );
2875 $refunds_by_day[ $donation_day ]['amount'] = $refunded_donation_amount;
2876 $refunds_by_day[ $donation_day ]['donors'] = $refunded_donation_count;
2877 }
2878
2879 return array(
2880 'donations' => $refunded_donations,
2881 'total_amount' => $refunded_donations_total_amount,
2882 'total_count' => $refunded_donations_total_count,
2883 'refunds_by_day' => $refunds_by_day,
2884 );
2885 }
2886
2887
2888 /**
2889 * This accepts a report type and date and attemps to create a report that has multiple data points.
2890 *
2891 * @param string $start_date The start date.
2892 * @param string $end_date The end date.
2893 *
2894 * @since 1.8.1
2895 */
2896 public function get_donation_breakdown_data( $start_date = false, $end_date = false ) { // phpcs:ignore
2897
2898 global $wpdb;
2899
2900 // Get all posts with a custom post type of 'donation' that were created on and including between $start_data and $end_date.
2901 $donations = $wpdb->get_results( // phpcs:ignore
2902 $wpdb->prepare(
2903 "SELECT SUM(cd.amount) AS total_amount, p.ID FROM $wpdb->posts p
2904 LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id
2905 WHERE p.post_type = 'donation' AND p.post_status = 'charitable-completed'
2906 GROUP BY p.ID"
2907 ) // phpcs:ignore
2908 );
2909
2910 // 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.
2911 $donations_by_day = $wpdb->get_results( // phpcs:ignore
2912 $wpdb->prepare(
2913 "SELECT SUM(cd.amount) AS total_amount, COUNT(p.ID) AS total_donors, DATE(p.post_date) AS post_date FROM $wpdb->posts p
2914 LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id
2915 WHERE p.post_type = 'donation' AND p.post_status = 'charitable-completed'
2916 GROUP BY DATE(p.post_date)"
2917 ) // phpcs:ignore
2918 );
2919 }
2920
2921
2922 /**
2923 * This gathers info from the database with a focus on donors.
2924 *
2925 * @param string $slug Slug.
2926 * @param string $start_date The start date.
2927 * @param string $end_date The end date.
2928 * @param string $return_as The return type. Object or array. Default is object.
2929 *
2930 * @since 1.8.1
2931 */
2932 public function get_donors_from_db( $slug = 'compare_from', $start_date = false, $end_date = false, $return_as = 'object' ) {
2933
2934 // what we need:
2935 //
2936 // Total Donation count between start_date and end_date.
2937 // Total Donation amount between start_date and end_date.
2938
2939 global $wpdb;
2940
2941 $return_as = ( 'array' === $return_as ) ? 'ARRAY_A' : 'OBJECT_K';
2942
2943 return ( $wpdb->get_results( // phpcs:ignore
2944 $wpdb->prepare(
2945 "SELECT cd.donor_id AS donor_id, COUNT(ccd.donation_id) as %s, SUM(ccd.amount) AS %s,
2946 cd.user_id AS user_id,
2947 cd.email AS donor_email,
2948 cd.first_name AS donor_first_name,
2949 cd.last_name AS donor_last_name
2950 FROM {$wpdb->prefix}charitable_donors cd
2951 LEFT JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON ccd.donor_id = cd.donor_id
2952 LEFT JOIN {$wpdb->prefix}posts p ON p.ID = ccd.donation_id
2953 WHERE p.post_date >= %s AND p.post_date <= %s AND p.post_status = 'charitable-completed'
2954 GROUP BY cd.donor_id",
2955 'total_donation_count_' . $slug,
2956 'total_donation_amount_' . $slug,
2957 $start_date,
2958 $end_date
2959 ),
2960 $return_as
2961 ) );
2962 }
2963
2964 /**
2965 * Generates HTML for almost the entire activities area.
2966 *
2967 * @since 1.8.1
2968 *
2969 * @param string $report_type The report type.
2970 * @param array $advanced_report_data The report data.
2971 * @param array $report_args The report args.
2972 * @param bool $show_actions Whether to show actions.
2973 *
2974 * @return string
2975 */
2976 public function generate_advanced_breakdown_report_html( $report_type = 'lybunt', $advanced_report_data = array(), $report_args = array(), $show_actions = false ) {
2977
2978 $table_meta = (array) $this->get_advanced_report_table_meta( $report_type, $report_args, $show_actions );
2979
2980 // Allow certain HTML tags in the column titles.
2981 $title_whitelist_tags = array(
2982
2983 'br' => array(),
2984
2985 );
2986
2987 ob_start();
2988
2989 ?>
2990
2991 <div class="charitable-headline-reports">
2992
2993
2994 <?php
2995 if ( ! charitable_is_pro() ) :
2996 ?>
2997 <div class="restricted-access-overlay"></div><?php endif; ?>
2998
2999 <?php echo $this->generate_advanced_cards( $advanced_report_data, $report_type, $report_args ); // phpcs:ignore ?>
3000
3001 </div>
3002
3003 <div class="tablenav charitable-section no-bottom">
3004
3005 <div class="alignleft actions">
3006
3007 <h2><?php echo esc_html( $table_meta['title'] ); ?></h2>
3008
3009 </div>
3010
3011 <div class="alignright">
3012
3013 <?php if ( ! charitable_is_pro() ) : ?>
3014
3015 <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>
3016
3017 <?php else : ?>
3018
3019 <form action="" method="post" class="charitable-advanced-download-form" id="charitable-advanced-download-form">
3020 <input name="charitable_report_action" type="hidden" value="charitable_report_download_advanced">
3021 <input name="start_date_compare_from" type="hidden" value="<?php echo esc_html( $report_args['start_date_compare_from'] ); ?>">
3022 <input name="end_date_compare_from" type="hidden" value="<?php echo esc_html( $report_args['end_date_compare_from'] ); ?>">
3023 <input name="start_date_compare_to" type="hidden" value="<?php echo esc_html( $report_args['start_date_compare_to'] ); ?>">
3024 <input name="end_date_compare_to" type="hidden" value="<?php echo esc_html( $report_args['end_date_compare_to'] ); ?>">
3025 <input name="campaign_id" type="hidden" value="-1">
3026 <input name="report_type" type="hidden" value="<?php echo esc_html( $report_type ); ?>">
3027 <?php wp_nonce_field( 'charitable_export_report', 'charitable_export_report_nonce' ); ?>
3028 <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>
3029 </form>
3030
3031 <?php endif; ?>
3032
3033 </div>
3034
3035 </div>
3036
3037 <br class="clear">
3038
3039 <div class="charitable-report-table-container">
3040
3041 <?php do_action( 'charitable_report_before_advanced_table' ); ?>
3042
3043 <?php
3044 if ( ! charitable_is_pro() ) :
3045 ?>
3046 <div class="charitable-restricted"><div class="restricted-access-overlay"></div><?php endif; ?>
3047
3048 <table class="wp-list-table widefat fixed striped table-view-list donations-breakdown charitable-report-ui">
3049 <thead>
3050 <tr>
3051 <?php
3052 if ( ! empty( $table_meta['columns'] ) ) :
3053 foreach ( $table_meta['columns'] as $column_slug => $column_title ) :
3054 if ( $column_slug === 'avatar' ) {
3055 echo '<th style="width: 32px;" class="charitable-avatar"></th>';
3056 } else {
3057 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>';
3058 }
3059 endforeach;
3060 endif;
3061 ?>
3062 </tr>
3063 </thead>
3064 <tbody id="donations-breakdown-list">
3065 <?php echo $this->generate_advanced_breakdown_rows( $advanced_report_data, $report_type, count( $table_meta['columns'], $show_actions ) ); // phpcs:ignore ?>
3066 </tbody>
3067 <tfoot>
3068 <tr>
3069 <?php
3070 foreach ( $table_meta['columns'] as $column_slug => $column_title ) :
3071 if ( $column_slug === 'avatar' ) {
3072 echo '<th style="width: 32px;" class="charitable-avatar"></th>';
3073 } else {
3074 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>';
3075 }
3076 endforeach;
3077 ?>
3078 </tr>
3079 </tfoot>
3080 </table>
3081
3082 <?php
3083 if ( ! charitable_is_pro() ) :
3084 ?>
3085 </div><?php endif; ?>
3086
3087 <?php
3088
3089 do_action( 'charitable_report_after_advanced_table' );
3090
3091 ?>
3092 </div>
3093 <?php
3094
3095 return ob_get_clean();
3096 }
3097
3098 /**
3099 * Determines what function to generate table rows for advanced based on the report type.
3100 *
3101 * @since 1.8.1
3102 *
3103 * @param array $advanced_report_data The report data.
3104 * @param string $report_type The report type.
3105 * @param int $number_of_columns The number of columns.
3106 * @param bool $show_actions Whether to show actions.
3107 *
3108 * @return string
3109 */
3110 public function generate_advanced_breakdown_rows( $advanced_report_data = array(), $report_type = 'lybunt', $number_of_columns = false, $show_actions = false ) {
3111
3112 $rows = false;
3113
3114 switch ( $report_type ) {
3115 case 'lybunt':
3116 $rows = $this->generate_lybunt_rows( $advanced_report_data, $number_of_columns, $show_actions );
3117 break;
3118
3119 default:
3120 // code...
3121 break;
3122 }
3123
3124 return $rows;
3125 }
3126
3127 /**
3128 * Generates HTML for the "report cards" at the top of the advanced page.
3129 *
3130 * @since 1.8.1
3131 *
3132 * @param array $advanced_report_data The report data.
3133 * @param string $report_type The report type.
3134 * @param array $report_args The report args.
3135 *
3136 * @return string
3137 */
3138 public function generate_advanced_cards( $advanced_report_data = array(), $report_type = 'lybunt', $report_args = array() ) {
3139
3140 ob_start();
3141
3142 if ( 'lybunt' === $report_type ) :
3143
3144 if ( charitable_is_pro() ) :
3145
3146 if ( ! is_array( $advanced_report_data ) ) {
3147 $advanced_report_data = array();
3148 }
3149
3150 // count all elements in a multi-dimenstional array that has a particular value greator than 0.
3151 $total_count_donors_from = number_format(
3152 count(
3153 array_filter(
3154 array_column( $advanced_report_data, 'total_donation_count_compare_from' ),
3155 function ( $value ) {
3156 return $value > 0;
3157 }
3158 )
3159 )
3160 );
3161 $total_count_donors_to = number_format(
3162 count(
3163 array_filter(
3164 array_column( $advanced_report_data, 'total_donation_count_compare_to' ),
3165 function ( $value ) {
3166 return $value > 0;
3167 }
3168 )
3169 )
3170 );
3171
3172 $total_amount_donations_from = array_sum( array_column( $advanced_report_data, 'total_donation_amount_compare_from' ) );
3173 $total_amount_donations_to = array_sum( array_column( $advanced_report_data, 'total_donation_amount_compare_to' ) );
3174
3175 // if value does not have decimals, then add two decimal places.
3176 $total_amount_donations_from = ( false === strpos( $total_amount_donations_from, '.' ) ) ? $total_amount_donations_from . '.00' : $total_amount_donations_from;
3177 $total_amount_donations_to = ( false === strpos( $total_amount_donations_to, '.' ) ) ? $total_amount_donations_to . '.00' : $total_amount_donations_to;
3178
3179 $from_date_range = '( ' . $report_args['start_date_compare_from'] . ' - ' . $report_args['end_date_compare_from'] . ' )';
3180 $to_date_range = '( ' . $report_args['start_date_compare_to'] . ' - ' . $report_args['end_date_compare_to'] . ' )';
3181
3182 else :
3183
3184 $total_count_donors_from = number_format( 143 );
3185 $total_count_donors_to = number_format( 1332 );
3186 $total_amount_donations_from = '$500.00';
3187 $total_amount_donations_to = '$25,040.00';
3188 $from_date_range = '2022/01/01 - 2022/12/31';
3189 $to_date_range = '2023/01/01 - 2023/12/31';
3190
3191 endif;
3192
3193
3194 ?>
3195
3196 <div class="charitable-cards">
3197 <div class="charitable-container charitable-report-ui charitable-card">
3198 <strong><span id="charitable-top-donor-count"><?php echo ( intval( $total_count_donors_from ) ); ?></span></strong>
3199 <p><?php echo esc_html__( 'Donors', 'charitable' ); ?></p>
3200 <p><?php echo esc_html( $from_date_range ); ?></p>
3201 </div>
3202 <div class="charitable-container charitable-report-ui charitable-card">
3203 <strong><span id="charitable-top-donor-count"><?php echo ( intval( $total_count_donors_to ) ); ?></span></strong>
3204 <p><?php echo esc_html__( 'Donors', 'charitable' ); ?></p>
3205 <p><?php echo esc_html( $to_date_range ); ?></p>
3206 </div>
3207 <div class="charitable-container charitable-report-ui charitable-card">
3208 <strong><span id="charitable-top-donor-count"><?php echo charitable_format_money( $total_amount_donations_from, 2, true ); // phpcs:ignore ?></span></strong>
3209 <p><?php echo esc_html__( 'Donations', 'charitable' ); ?></p>
3210 <p><?php echo esc_html( $from_date_range ); ?></p>
3211 </div>
3212 <div class="charitable-container charitable-report-ui charitable-card">
3213 <strong><span id="charitable-top-donor-count"><?php echo charitable_format_money( $total_amount_donations_to, 2, true ); // phpcs:ignore ?></span></strong>
3214 <p><?php echo esc_html__( 'Donations', 'charitable' ); ?></p>
3215 <p><?php echo esc_html( $to_date_range ); ?></p>
3216 </div>
3217 </div>
3218
3219 <?php
3220
3221 endif;
3222
3223 return ob_get_clean();
3224 }
3225
3226 /**
3227 * Generates HTML for the "report table" at the middle of the advanced page.
3228 *
3229 * @since 1.8.1
3230 *
3231 * @param array $report_type The report type.
3232 * @param array $report_args The report args.
3233 * @param array $show_actions Whether to show actions.
3234 *
3235 * @return string
3236 */
3237 public function get_advanced_report_table_meta( $report_type = 'lybunt', $report_args = array(), $show_actions = false ) {
3238
3239 $table_meta = array();
3240
3241 switch ( $report_type ) {
3242 case 'lybunt':
3243 $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'] ) : '';
3244 $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'] ) : '';
3245
3246 $table_meta = array(
3247 'title' => esc_html__( 'Donors', 'charitable' ),
3248 'columns' => array(
3249 'avatar' => '',
3250 'name-email' => esc_html__( 'Name', 'charitable' ),
3251 'total-donations' => esc_html__( 'Last Donation', 'charitable' ),
3252 'number-donations-1' => esc_html__( 'Total Donations', 'charitable' ) . '<br/>' . $number_donations_1_date_range,
3253 'number-donations-2' => esc_html__( 'Total Donations', 'charitable' ) . '<br/>' . $number_donations_2_date_range,
3254 'last-campaign' => esc_html__( 'Last Campaign', 'charitable' ),
3255 'life-total' => esc_html__( 'Life Total', 'charitable' ),
3256 ),
3257 );
3258
3259 if ( $show_actions ) {
3260 $table_meta['columns']['actions'] = esc_html__( 'Actions', 'charitable' );
3261 }
3262
3263 break;
3264
3265 default:
3266 // code...
3267 break;
3268 }
3269
3270 return $table_meta;
3271 }
3272
3273 /**
3274 * This gets report args and an iniital report.
3275 *
3276 * @param string $report_args The report args.
3277 *
3278 * @since 1.8.1
3279 *
3280 * @return array
3281 */
3282 public function get_lybunt_data( $report_args = array() ) {
3283
3284 global $wpdb;
3285
3286 $start_date_compare_from = esc_html( $report_args['start_date_compare_from'] );
3287 $end_date_compare_from = esc_html( $report_args['end_date_compare_from'] );
3288 $start_date_compare_to = esc_html( $report_args['start_date_compare_to'] );
3289 $end_date_compare_to = esc_html( $report_args['end_date_compare_to'] );
3290 $limit = ! empty( $report_args['limit'] ) ? intval( $report_args['limit'] ) : apply_filters( 'charitable_report_advanced_lybunt_limit', 100 );
3291 $order_by = ! empty( $report_args['order_by'] ) ? esc_html( $report_args['order_by'] ) : 'total_life_amount';
3292 $order = ! empty( $report_args['order'] ) ? esc_html( $report_args['order'] ) : 'DESC';
3293
3294 $_donors = array();
3295
3296 // Determine if this particular report has been cached recently.
3297 // For now though we are not caching.
3298 $donors = $this->maybe_cache_report( false ) ? false : $this->get_cached_report( 'lybunt', $start_date_compare_from, $end_date_compare_to, false, false, false );
3299
3300 if ( false === $donors ) {
3301
3302 // Get the foundation of the report.
3303 $donors_compare_from = (array) $this->get_donors_from_db( 'compare_from', $start_date_compare_from, $end_date_compare_from, 'array' );
3304 $donors_compare_to = (array) $this->get_donors_from_db( 'compare_to', $start_date_compare_to, $end_date_compare_to, 'array' );
3305
3306 // Merge the two arrays - from and to.
3307 if ( ! empty( $donors_compare_from ) ) :
3308
3309 foreach ( $donors_compare_from as $key => $donor ) :
3310 $_donors[ $donor['donor_id'] ] = $donor;
3311 endforeach;
3312
3313 endif;
3314
3315 if ( ! empty( $donors_compare_to ) ) :
3316
3317 foreach ( $donors_compare_to as $key => $donor ) :
3318 // if donor_id exists in $_donors, merge the two arrays.
3319 if ( is_array( $_donors ) && array_key_exists( $donor['donor_id'], $_donors ) ) {
3320 $_donors[ $donor['donor_id'] ] = array_merge( $_donors[ $donor['donor_id'] ], $donor );
3321 } else {
3322 $_donors[ $donor['donor_id'] ] = $donor;
3323 }
3324 endforeach;
3325
3326 endif;
3327
3328 if ( empty( $_donors ) ) {
3329 return false;
3330 }
3331
3332 // Convert array of arrays to array of objects, keeping the array keys.
3333 $_donors = array_map(
3334 function ( $donor ) {
3335 return (object) $donor;
3336 },
3337 $_donors
3338 );
3339
3340 $donors = array();
3341
3342 foreach ( $_donors as $key => $donor ) {
3343 if ( ! empty( $donor->donor_id ) && 0 !== intval( $donor->donor_id ) ) :
3344 $donors[ $donor->donor_id ] = $donor;
3345 endif;
3346 }
3347
3348 foreach ( $donors as $donor_id => $donor_info ) {
3349
3350 $donor = new Charitable_Donor( $donor_id );
3351 $last_donation = $donor->get_last_donation();
3352 $last_donation_amount = $last_donation->get_total_donation_amount();
3353 $last_donation_campaign_links = $last_donation->get_campaigns_links();
3354 $donors[ $donor_id ]->last_donation_amount = $last_donation_amount;
3355 $donors[ $donor_id ]->last_donation_campaign_links = $last_donation_campaign_links;
3356 $donors[ $donor_id ]->total_life_amount = $donor->get_amount();
3357
3358 if ( empty( $donors[ $donor_id ]->total_donation_count_compare_from ) ) {
3359 $donors[ $donor_id ]->total_donation_count_compare_from = 0;
3360 }
3361 if ( empty( $donors[ $donor_id ]->total_donation_amount_compare_from ) ) {
3362 $donors[ $donor_id ]->total_donation_amount_compare_from = 0;
3363 }
3364 if ( empty( $donors[ $donor_id ]->total_donation_count_compare_to ) ) {
3365 $donors[ $donor_id ]->total_donation_count_compare_to = 0;
3366 }
3367 if ( empty( $donors[ $donor_id ]->total_donation_amount_compare_to ) ) {
3368 $donors[ $donor_id ]->total_donation_amount_compare_to = 0;
3369 }
3370 }
3371
3372 // Sort the data.
3373 if ( is_array( $donors ) && count( $donors ) > 0 ) :
3374 $donors = $this->sort_lybunt_data( $donors, $order_by, $order );
3375 endif;
3376
3377 // Reduce the array to the limit.
3378 if ( $limit && is_array( $donors ) && count( $donors ) > $limit ) :
3379 $donors = array_slice( $donors, 0, $limit, true );
3380 endif;
3381
3382 // Save this to the cache.
3383 $this->set_cached_report( 'lybunt', $start_date_compare_from, $end_date_compare_to, false, false, false, $donors );
3384
3385 }
3386
3387 return $donors;
3388 }
3389
3390 /**
3391 * Sorts the lybunt data.
3392 *
3393 * @since 1.8.1.3
3394 *
3395 * @param array $donors The donors data.
3396 * @param string $order_by The order by. Default is total_life_amount.
3397 * @param string $order The order (DESC OR ASC). Default is DESC.
3398 *
3399 * @return array
3400 */
3401 public function sort_lybunt_data( $donors = array(), $order_by = 'total_life_amount', $order = 'DESC' ) {
3402
3403 if ( ! empty( $donors ) ) :
3404
3405 usort(
3406 $donors,
3407 function ( $a, $b ) use ( $order_by, $order ) {
3408 if ( $order === 'DESC' && ! empty( $a->$order_by ) && ! empty( $b->$order_by ) ) {
3409 return $b->$order_by <=> $a->$order_by;
3410 } elseif ( $order === 'ASC' && ! empty( $a->$order_by ) && ! empty( $b->$order_by ) ) {
3411 return $a->$order_by <=> $b->$order_by;
3412 } else {
3413 return;
3414 }
3415 }
3416 );
3417
3418 endif;
3419
3420 return $donors;
3421 }
3422
3423 /**
3424 * Generates HTML rows for doantion breakdown table.
3425 *
3426 * @since 1.8.1
3427 *
3428 * @param array $donors_data The donors data.
3429 * @param int $number_of_columns The number of columns.
3430 * @param bool $show_actions Whether to show actions.
3431 *
3432 * @return string
3433 */
3434 public function generate_lybunt_rows( $donors_data = array(), $number_of_columns = false, $show_actions = false ) {
3435
3436 ob_start();
3437
3438 if ( empty( $donors_data ) ) {
3439 ?>
3440
3441 <td colspan="<?php echo intval( $number_of_columns ); ?>"><strong><?php echo esc_html__( 'There are no donations within the date range.', 'charitable' ); ?></strong></td>
3442
3443 <?php
3444
3445 } else {
3446
3447 foreach ( $donors_data as $donor_id => $data ) :
3448
3449 $donor_name = $data->donor_first_name . ' ' . $data->donor_last_name;
3450 $donor_email = $data->donor_email;
3451 $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . '/images/misc/placeholder-avatar-small.jpg';
3452 $total_donation_count_compare_to = $data->total_donation_count_compare_to;
3453 $total_donation_amount_compare_to = number_format( $data->total_donation_amount_compare_to, 2 );
3454 $total_donation_count_compare_from = $data->total_donation_count_compare_from;
3455 $total_donation_amount_compare_from = number_format( $data->total_donation_amount_compare_from, 2 );
3456 $last_donation_amount = $data->last_donation_amount;
3457 $total_life_amount = number_format( $data->total_life_amount, 2 );
3458 $campaign_links = implode( ',', $data->last_donation_campaign_links );
3459 $link = false;
3460 $actions_display = '';
3461
3462 ?>
3463
3464 <tr id="type-lybunt-donor-<?php echo intval( $donor_id ); ?>" class="row-type-lybunt">
3465 <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>
3466 <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>
3467 <td class="donated column-donations" data-colname="Donations"><?php echo charitable_format_money( $last_donation_amount ); // phpcs:ignore ?></td>
3468 <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>
3469 <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>
3470 <td class="donated column-refunds" data-colname="Refunds"><?php echo $campaign_links; // phpcs:ignore ?></td>
3471 <td class="donated column-net " data-colname="Net"><?php echo charitable_format_money( $total_life_amount, 2, true ); // phpcs:ignore ?></td>
3472 <?php
3473
3474 if ( $show_actions ) :
3475 if ( $link && charitable_is_pro() ) :
3476
3477 $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>';
3478
3479 else :
3480
3481 $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>';
3482
3483 endif;
3484 endif;
3485
3486 echo $actions_display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
3487 ?>
3488
3489 </tr>
3490
3491 <?php
3492 endforeach;
3493
3494 }
3495
3496 return ob_get_clean();
3497 }
3498
3499 /**
3500 * Return the HTML of the filter select dropdown.
3501 *
3502 * @since 1.8.1
3503 *
3504 * @param string $selected_value The selected value.
3505 *
3506 * @return string
3507 */
3508 public function get_advanced_report_type_dropdown( $selected_value = false ) {
3509
3510 $options_values = apply_filters(
3511 'charitable_reports_advanced_report_type_filter_values',
3512 array(
3513 'lybunt' => __( 'LYBUNT / SYBUNT', 'charitable' ),
3514 )
3515 );
3516
3517 ob_start();
3518
3519 ?>
3520
3521 <select autocomplete="off" name="action" id="report-advanced-type-filter">
3522 <?php foreach ( $options_values as $value => $label ) : ?>
3523 <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_value, $value ); ?>><?php echo esc_html( $label ); ?></option>
3524 <?php endforeach; ?>
3525 </select>
3526
3527 <?php
3528
3529 return ob_get_clean();
3530 }
3531
3532 /**
3533 * Get the donors for the dedicated advanced report.
3534 *
3535 * @since 1.8.1
3536 *
3537 * @param string $report_args The report args.
3538 *
3539 * @return array|bool
3540 */
3541 public function get_advanced_data_by_report_type( $report_args = array() ) {
3542
3543 if ( empty( $report_args['report_type'] ) ) {
3544 return false;
3545 }
3546
3547 switch ( sanitize_title( $report_args['report_type'] ) ) {
3548 case 'lybunt':
3549 return ( $this->get_lybunt_data( $report_args ) );
3550 break; // phpcs:ignore
3551
3552 default:
3553 return false;
3554 break; // phpcs:ignore
3555 }
3556 }
3557
3558 /**
3559 * Return net value
3560 *
3561 * @since 1.8.1
3562 *
3563 * @param float $donation_amount The donation amount.
3564 * @param float $refund_amount The refund amount.
3565 *
3566 * @return float
3567 */
3568 public function get_net( $donation_amount = 0, $refund_amount = 0 ) {
3569
3570 // If the donation amount is less than the refund amount, return 0.
3571 if ( $donation_amount < $refund_amount ) {
3572 return 0;
3573 }
3574
3575 return $donation_amount - $refund_amount;
3576 }
3577
3578 /**
3579 * Amount formatting for reports - includes currency symbol.
3580 *
3581 * @since 1.8.1
3582 *
3583 * @param int $amount The amount.
3584 *
3585 * @return string
3586 */
3587 public function charitable_reports_format_money( $amount = 0 ) {
3588
3589 return html_entity_decode( charitable_format_money( $amount ) );
3590 }
3591
3592 /**
3593 * Returns if activities are empty.
3594 *
3595 * @since 1.8.1
3596 *
3597 * @return boolean
3598 */
3599 public function is_activities_empty() {
3600
3601 if ( $this->activities === false || empty( $this->activities ) ) {
3602 return true;
3603 }
3604
3605 return false;
3606 }
3607
3608 /**
3609 * Get localized strings.
3610 *
3611 * @since 1.8.1
3612 *
3613 * @return array
3614 */
3615 private function get_localized_strings() {
3616
3617 $charitable_reports_advanced_report_strings = apply_filters(
3618 'charitable_reports_advanced_report_strings',
3619 array(
3620 'lybunt' => array(
3621 'title' => __( 'LYBUNT / SYBUNT', 'charitable' ),
3622 'description' => wp_kses_post(
3623 __(
3624 '<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>
3625
3626 <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>',
3627 'charitable'
3628 ),
3629 array(
3630 'p' => array(),
3631 'strong' => array(),
3632 )
3633 ),
3634 ),
3635 )
3636 );
3637
3638 $currency_helper = charitable_get_currency_helper();
3639
3640 // Decode HTML entities in currency symbol to prevent double-encoding in JSON.
3641 // The currency symbol comes as HTML entities (e.g., &#36; for $), so we decode it
3642 // before passing to JavaScript to avoid double-encoding issues.
3643 // This ensures the currency symbol is a plain character that won't be double-encoded.
3644 $currency_symbol = $currency_helper->get_currency_symbol();
3645 // Decode HTML entities to get the plain character (e.g., &#36; becomes $).
3646 $currency_symbol = html_entity_decode( $currency_symbol, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
3647 $decimal_count = $currency_helper->get_decimals();
3648
3649 $strings = array(
3650 'version' => Charitable::VERSION,
3651 'nonce' => wp_create_nonce( 'charitable-reporting' ),
3652 'admin_nonce' => wp_create_nonce( 'charitable-admin' ),
3653 'currency_symbol' => $currency_symbol,
3654 'decimal_count' => (int) $decimal_count,
3655 'ajax_url' => admin_url( 'admin-ajax.php' ),
3656 'date_select_day' => 'DD',
3657 'date_select_month' => 'MM',
3658 'headline_chart_options' => array(
3659 'donation_axis' => (array) $this->donation_axis,
3660 'date_axis' => (array) $this->date_axis,
3661 ),
3662 'payment_methods_chart_options' => array(
3663 'payment_percentages' => (array) array_reverse( $this->payment_percentages ),
3664 'payment_labels' => (array) array_reverse( $this->payment_labels ),
3665 'payment_keys' => (array) array_reverse( $this->payment_keys ),
3666 ),
3667 'default_start_date' => current_time( gmdate( 'Y-m-d', strtotime( '-1 month' ) ) ),
3668 'default_end_date' => current_time( gmdate( 'Y-m-d' ) ),
3669 'advanced_reports' => $charitable_reports_advanced_report_strings,
3670 );
3671
3672 $strings = apply_filters( 'charitable_reporting_strings', $strings );
3673
3674 return $strings;
3675 }
3676
3677 /**
3678 * Return the array of tabs used on the settings page.
3679 *
3680 * @since 1.8.1
3681 *
3682 * @return string[]
3683 */
3684 public function get_sections() {
3685 /**
3686 * Filter the settings tabs.
3687 *
3688 * @since 1.8.1
3689 *
3690 * @param string[] $tabs List of tabs in key=>label format.
3691 */
3692 return apply_filters(
3693 'charitable_reports_tabs',
3694 array(
3695 'overview' => __( 'Overview', 'charitable' ),
3696 'advanced' => __( 'Advanced', 'charitable' ),
3697 'activity' => __( 'Activity', 'charitable' ),
3698 'donors' => __( 'Donors', 'charitable' ),
3699 'analytics' => __( 'Analytics', 'charitable' ),
3700 )
3701 );
3702 }
3703
3704 /**
3705 * Return the array of terms for custom campaign categories.
3706 *
3707 * @since 1.8.1
3708 *
3709 * @return string[]
3710 */
3711 public function get_campaign_categories() {
3712
3713 return apply_filters(
3714 'charitable_reports_filter_campaign_categories',
3715 get_terms(
3716 'campaign_category'
3717 )
3718 );
3719 }
3720
3721 /**
3722 * Provides the content (title/description) to display inside the title card for a particular report.
3723 *
3724 * @since 1.8.1
3725 *
3726 * @param string $report_type The report type.
3727 *
3728 * @return string[]
3729 */
3730 public function generate_title_card_html( $report_type = false ) {
3731
3732 if ( false === $report_type ) {
3733 return;
3734 }
3735
3736 switch ( sanitize_title( $report_type ) ) {
3737 case 'donors-top':
3738 $title = __( 'Top Donors', 'charitable' );
3739 // use wp_kses_post to allow html in the string.
3740 $description = wp_kses_post(
3741 __(
3742 '<p>This report shows which donors have made the largest gifts and donations.</p>
3743 <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>
3744 <p>You can also use this report to thank top donors for their support.</p>',
3745 'charitable'
3746 ),
3747 array(
3748 'p' => array(),
3749 'strong' => array(),
3750 )
3751 );
3752 break;
3753
3754 case 'donors-recurring':
3755 $title = __( 'Recurring Donors', 'charitable' );
3756 // use wp_kses_post to allow html in the string.
3757 $description = wp_kses_post(
3758 __(
3759 '<p>This report shows which donors have made repeated donations the largest gifts and donations.</p>
3760 <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>
3761 <p>You can also use this report to thank recurring donors for their support.</p>',
3762 'charitable'
3763 ),
3764 array(
3765 'p' => array(),
3766 'strong' => array(),
3767 )
3768 );
3769 break;
3770
3771 case 'donors-first-time':
3772 $title = __( 'First Time Donors', 'charitable' );
3773 // use wp_kses_post to allow html in the string.
3774 $description = wp_kses_post(
3775 __(
3776 '<p>This report shows which donors have made one donation - their first.</p>
3777 <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>
3778 <p>You can also use this report to thank first time donors for their support.</p>',
3779 'charitable'
3780 ),
3781 array(
3782 'p' => array(),
3783 'strong' => array(),
3784 )
3785 );
3786 break;
3787
3788 case 'lybunt':
3789 $title = __( 'LYBUNT / SYBUNT', 'charitable' );
3790 $description = wp_kses_post(
3791 __(
3792 '<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>
3793
3794 <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>',
3795 'charitable'
3796 ),
3797 array(
3798 'p' => array(),
3799 'strong' => array(),
3800 )
3801 );
3802 break;
3803
3804 default:
3805 // code...
3806 break;
3807 }
3808
3809 ob_start();
3810
3811 ?>
3812 <h2><?php echo esc_html( $title ); ?></h2>
3813 <div class="charitable-report-description"><?php echo $description; // phpcs:ignore ?></div>
3814 <?php
3815
3816 return ob_get_clean();
3817 }
3818
3819 /**
3820 * Return the HTML of the filter select dropdown.
3821 *
3822 * @since 1.8.1
3823 *
3824 * @param string $selected_value The selected value.
3825 *
3826 * @return string
3827 */
3828 public function get_filter_dropdown( $selected_value = false ) {
3829
3830 $options_values = $this->date_filter_values;
3831
3832 ob_start();
3833
3834 ?>
3835
3836 <select name="action" id="report-date-range-filter">
3837 <?php foreach ( $options_values as $value => $label ) : ?>
3838 <option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_value, $value ); ?>><?php echo esc_html( $label ); ?></option>
3839 <?php endforeach; ?>
3840 </select>
3841
3842 <?php
3843
3844 return ob_get_clean();
3845 }
3846
3847 /**
3848 * Return sample data for the example tables.
3849 *
3850 * @since 1.8.1
3851 *
3852 * @param string $report_type The report type.
3853 * @param string $data_type The data type. Object or array.
3854 * @param int $number_of_records The number of records to return. Default 10.
3855 *
3856 * @return array
3857 */
3858 public function get_data_sample( $report_type = false, $data_type = 'object', $number_of_records = 10 ) {
3859
3860 if ( false === $report_type ) {
3861 return;
3862 }
3863
3864 if ( $data_type === 'object' ) {
3865 $data = new stdClass();
3866 } else {
3867 $data = array();
3868 }
3869
3870 if ( $this->is_report_type_donor( $report_type ) ) {
3871
3872 for ( $x = 0; $x < $number_of_records; $x++ ) {
3873
3874 if ( $data_type === 'object' ) {
3875
3876 $random_price = wp_rand( 50, 10000 ) / 10;
3877
3878 $data->{$x} = new stdClass();
3879
3880 $data->{$x}->total_amount = $random_price;
3881 $data->{$x}->average = $random_price;
3882
3883 $data->{$x}->total_count_donations = 1;
3884 $data->{$x}->total_count_campaigns = 1;
3885 $data->{$x}->last_donation_date = '2023-11-28 16:59:33';
3886 $data->{$x}->last_campaign_title = 'My New Campaign 1';
3887 $data->{$x}->last_campaign_id = 0;
3888 $data->{$x}->donor_id = 0;
3889 $data->{$x}->user_id = 0;
3890 $data->{$x}->email = 'example_donor_' . $x . '@wpcharitable.com';
3891 $data->{$x}->first_name = 'Roger';
3892 $data->{$x}->last_name = 'Wilco';
3893 $data->{$x}->date_joined = '2023-11-28 16:59:33';
3894 $data->{$x}->donation_id = 0;
3895 $data->{$x}->campaign_id = 0;
3896 $data->{$x}->amount = $random_price;
3897 $data->{$x}->sample = true;
3898
3899 } else {
3900
3901 $random_price_1 = mt_rand( 50, 1000 ) / 10; // phpcs:ignore
3902
3903 $data[] = array(
3904
3905 'total_amount' => $random_price_1,
3906 'average' => $random_price_1,
3907 'total_count_donations' => 1,
3908 'total_count_campaigns' => 1,
3909 'last_donation_date' => '2023-11-28 16:59:33',
3910 'last_campaign_title' => 'My New Campaign 1',
3911 'last_campaign_id' => 0,
3912 'donor_id' => 0,
3913 'user_id' => 0,
3914 'email' => 'example_donor_' . $x . '@wpcharitable.com',
3915 'first_name' => 'Roger',
3916 'last_name' => 'Wilco',
3917 'date_joined' => '2024-01-28 16:59:33',
3918 'donation_id' => 0,
3919 'campaign_id' => 0,
3920 'amount' => $random_price_1,
3921 'sample' => true,
3922 );
3923 }
3924 }
3925
3926 } elseif ( $report_type === 'advanced' ) {
3927
3928 $random_price_1 = wp_rand( 50, 10000 ) / 10;
3929 $random_price_2 = wp_rand( 50, 10000 ) / 10;
3930 $random_price_3 = wp_rand( 50, 10000 ) / 10;
3931 $random_price_4 = wp_rand( 50, 10000 ) / 10;
3932
3933 $data = array(
3934 1 => (object) array(
3935 'donor_id' => 1,
3936 'total_donation_count_compare_to' => 3,
3937 'total_donation_amount_compare_to' => $random_price_1,
3938 'user_id' => 0,
3939 'donor_email' => '[email protected]',
3940 'donor_first_name' => 'David',
3941 'donor_last_name' => 'Bisset',
3942 'last_donation_amount' => $random_price_2,
3943 'last_donation_campaign_links' => array(
3944 34 => 'Help the Children Campaign',
3945 ),
3946 'total_life_amount' => 678,
3947 'total_donation_count_compare_from' => 0,
3948 'total_donation_amount_compare_from' => 0,
3949 'sample' => true,
3950 ),
3951 2 => (object) array(
3952 'donor_id' => 2,
3953 'total_donation_count_compare_to' => 3,
3954 'total_donation_amount_compare_to' => $random_price_3,
3955 'user_id' => 0,
3956 'donor_email' => '[email protected]',
3957 'donor_first_name' => 'Sarah',
3958 'donor_last_name' => 'Smith',
3959 'last_donation_amount' => $random_price_4,
3960 'last_donation_campaign_links' => array(
3961 34 => 'Help the Children Campaign',
3962 ),
3963 'total_life_amount' => $random_price_3,
3964 'total_donation_count_compare_from' => 0,
3965 'total_donation_amount_compare_from' => 0,
3966 'sample' => true,
3967 ),
3968 3 => (object) array(
3969 'donor_id' => 1,
3970 'total_donation_count_compare_to' => 3,
3971 'total_donation_amount_compare_to' => $random_price_1,
3972 'user_id' => 0,
3973 'donor_email' => '[email protected]',
3974 'donor_first_name' => 'Jim',
3975 'donor_last_name' => 'Bisset',
3976 'last_donation_amount' => $random_price_2,
3977 'last_donation_campaign_links' => array(
3978 34 => 'Help the Children Campaign',
3979 ),
3980 'total_life_amount' => 678,
3981 'total_donation_count_compare_from' => 0,
3982 'total_donation_amount_compare_from' => 0,
3983 'sample' => true,
3984 ),
3985 4 => (object) array(
3986 'donor_id' => 2,
3987 'total_donation_count_compare_to' => 3,
3988 'total_donation_amount_compare_to' => $random_price_3,
3989 'user_id' => 0,
3990 'donor_email' => '[email protected]',
3991 'donor_first_name' => 'George',
3992 'donor_last_name' => 'Smith',
3993 'last_donation_amount' => $random_price_4,
3994 'last_donation_campaign_links' => array(
3995 34 => 'Help the Children Campaign',
3996 ),
3997 'total_life_amount' => $random_price_3,
3998 'total_donation_count_compare_from' => 0,
3999 'total_donation_amount_compare_from' => 0,
4000 'sample' => true,
4001 ),
4002 5 => (object) array(
4003 'donor_id' => 1,
4004 'total_donation_count_compare_to' => 3,
4005 'total_donation_amount_compare_to' => $random_price_1,
4006 'user_id' => 0,
4007 'donor_email' => '[email protected]',
4008 'donor_first_name' => 'Zoe',
4009 'donor_last_name' => 'Bisset',
4010 'last_donation_amount' => $random_price_2,
4011 'last_donation_campaign_links' => array(
4012 34 => 'Help the Children Campaign',
4013 ),
4014 'total_life_amount' => 678,
4015 'total_donation_count_compare_from' => 0,
4016 'total_donation_amount_compare_from' => 0,
4017 'sample' => true,
4018 ),
4019 6 => (object) array(
4020 'donor_id' => 2,
4021 'total_donation_count_compare_to' => 3,
4022 'total_donation_amount_compare_to' => $random_price_3,
4023 'user_id' => 0,
4024 'donor_email' => '[email protected]',
4025 'donor_first_name' => 'Ava',
4026 'donor_last_name' => 'Smith',
4027 'last_donation_amount' => $random_price_4,
4028 'last_donation_campaign_links' => array(
4029 34 => 'Help the Children Campaign',
4030 ),
4031 'total_life_amount' => $random_price_3,
4032 'total_donation_count_compare_from' => 0,
4033 'total_donation_amount_compare_from' => 0,
4034 'sample' => true,
4035 ),
4036 7 => (object) array(
4037 'donor_id' => 1,
4038 'total_donation_count_compare_to' => 3,
4039 'total_donation_amount_compare_to' => $random_price_1,
4040 'user_id' => 0,
4041 'donor_email' => '[email protected]',
4042 'donor_first_name' => 'Zoe',
4043 'donor_last_name' => 'Bisset',
4044 'last_donation_amount' => $random_price_2,
4045 'last_donation_campaign_links' => array(
4046 34 => 'Help the Children Campaign',
4047 ),
4048 'total_life_amount' => 678,
4049 'total_donation_count_compare_from' => 0,
4050 'total_donation_amount_compare_from' => 0,
4051 'sample' => true,
4052 ),
4053 8 => (object) array(
4054 'donor_id' => 2,
4055 'total_donation_count_compare_to' => 3,
4056 'total_donation_amount_compare_to' => $random_price_3,
4057 'user_id' => 0,
4058 'donor_email' => '[email protected]',
4059 'donor_first_name' => 'Ava',
4060 'donor_last_name' => 'Smith',
4061 'last_donation_amount' => $random_price_4,
4062 'last_donation_campaign_links' => array(
4063 34 => 'Help the Children Campaign',
4064 ),
4065 'total_life_amount' => $random_price_3,
4066 'total_donation_count_compare_from' => 0,
4067 'total_donation_amount_compare_from' => 0,
4068 'sample' => true,
4069 ),
4070 9 => (object) array(
4071 'donor_id' => 1,
4072 'total_donation_count_compare_to' => 3,
4073 'total_donation_amount_compare_to' => $random_price_1,
4074 'user_id' => 0,
4075 'donor_email' => '[email protected]',
4076 'donor_first_name' => 'David',
4077 'donor_last_name' => 'Bisset',
4078 'last_donation_amount' => $random_price_2,
4079 'last_donation_campaign_links' => array(
4080 34 => 'Help the Children Campaign',
4081 ),
4082 'total_life_amount' => 678,
4083 'total_donation_count_compare_from' => 0,
4084 'total_donation_amount_compare_from' => 0,
4085 'sample' => true,
4086 ),
4087 10 => (object) array(
4088 'donor_id' => 2,
4089 'total_donation_count_compare_to' => 3,
4090 'total_donation_amount_compare_to' => $random_price_3,
4091 'user_id' => 0,
4092 'donor_email' => '[email protected]',
4093 'donor_first_name' => 'Sarah',
4094 'donor_last_name' => 'Smith',
4095 'last_donation_amount' => $random_price_4,
4096 'last_donation_campaign_links' => array(
4097 34 => 'Help the Children Campaign',
4098 ),
4099 'total_life_amount' => $random_price_3,
4100 'total_donation_count_compare_from' => 0,
4101 'total_donation_amount_compare_from' => 0,
4102 'sample' => true,
4103 ),
4104 );
4105
4106 }
4107
4108 $data = apply_filters(
4109 'charitable_report_donation_sample_data',
4110 $data,
4111 $report_type,
4112 $data_type,
4113 $number_of_records
4114 );
4115
4116 return $data;
4117 }
4118
4119 /**
4120 * Determine if the report type is a particular type of donor report.
4121 *
4122 * @since 1.8.1
4123 *
4124 * @param string $report_type The report type.
4125 *
4126 * @return string
4127 */
4128 public function is_report_type_donor( $report_type = false ) {
4129 return ( strpos( $report_type, 'donors-' ) === 0 );
4130 }
4131
4132 /**
4133 * Checks and sees if the string is a valid date, and if so return it in Y-m-d format.
4134 *
4135 * @since 1.8.1
4136 *
4137 * @param string $date_string The date string.
4138 *
4139 * @return string
4140 */
4141 public function get_valid_date_string( $date_string = false ) {
4142
4143 // Determine if $date_string is a valid date.
4144 $parsed_date = gmdate( 'Y/m/d', strtotime( $date_string ) );
4145
4146 $date = DateTime::createFromFormat( 'Y/m/d', $parsed_date );
4147
4148 if ( $date && $date->format( 'Y/m/d' ) === $parsed_date ) {
4149 return $date->format( 'Y-m-d' );
4150 } else {
4151 return false;
4152 }
4153 }
4154
4155 /**
4156 * Display upgrade notice at the bottom on the plugin settings pages.
4157 *
4158 * @since 1.8.1
4159 * @version 1.8.8
4160 *
4161 * @param string $view Current view inside the plugin settings page.
4162 *
4163 * @return void
4164 */
4165 public function reports_cta( $view = false, $css_class = 'reports-lite-cta', $show_close_button = false ) { // phpcs:ignore
4166
4167 if ( charitable_is_pro() ) {
4168 // no need to display this cta since they have a valid license.
4169 return;
4170 }
4171
4172 if ( get_option( 'charitable_lite_reports_upgrade', false ) || apply_filters( 'charitable_lite_reports_upgrade', false ) ) {
4173 return;
4174 }
4175 ?>
4176 <?php charitable_render_global_upgrade_cta( $css_class ); ?>
4177 <?php
4178 }
4179
4180 /**
4181 * Dismiss upgrade notice at the bottom on the plugin settings pages.
4182 *
4183 * @since 1.8.1
4184 */
4185 public function reports_cta_dismiss() {
4186
4187 if ( ! charitable_current_user_can() ) {
4188 wp_send_json_error();
4189 }
4190
4191 update_option( 'charitable_lite_reports_upgrade', time() );
4192
4193 wp_send_json_success();
4194 }
4195
4196 /**
4197 * See if it's the right time to load scripts.
4198 *
4199 * @since 1.8.1
4200 *
4201 * @return void
4202 */
4203 public function maybe_load_scripts() {
4204
4205 $screen = get_current_screen();
4206
4207 // Add Javascript vars at the footer of the WordPress admin, at the dashboard and the overview tab of reporting.
4208 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
4209
4210 // Specific styles for the "overview" tab.
4211 add_action( 'admin_footer', [ $this, 'report_vars' ], 100 );
4212 }
4213 }
4214
4215 /**
4216 * See if it's the right time to load cta.
4217 *
4218 * @since 1.8.1
4219 *
4220 * @return void
4221 */
4222 public function maybe_add_reports_cta() {
4223 add_action( 'charitable_report_before_donor_table', [ $this, 'reports_cta' ] );
4224 add_action( 'charitable_report_before_activity_table', [ $this, 'reports_cta' ] );
4225 add_action( 'charitable_report_before_advanced_table', [ $this, 'reports_cta' ] );
4226 }
4227
4228 /**
4229 * Returns and/or create the single instance of this class.
4230 *
4231 * @since 1.8.1
4232 *
4233 * @return Charitable_Reports
4234 */
4235 public static function get_instance() {
4236 if ( is_null( self::$instance ) ) {
4237 self::$instance = new self();
4238 }
4239
4240 return self::$instance;
4241 }
4242 }
4243
4244 endif;
4245