PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 1.7.0.9 All 211 releases
charitable / includes / admin / donations / class-charitable-donation-list-table.php

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

1,216 lines 38.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Sets up the donations list table in the admin.
4 *
5 * @package Charitable/Classes/Charitable_Donation_List_Table
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.5.0
10 * @version 1.8.1.5, 1.8.9.1, 1.8.10
11 */
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! class_exists( 'Charitable_Donation_List_Table' ) ) :
18
19 /**
20 * Charitable_Donation_List_Table class.
21 *
22 * @final
23 * @since 1.5.0
24 */
25 final class Charitable_Donation_List_Table {
26
27 /**
28 * The single instance of this class.
29 *
30 * @var Charitable_Donation_List_Table|null
31 */
32 private static $instance = null;
33
34 /**
35 * Post type.
36 *
37 * @var string
38 */
39 protected $list_table_type = 'donation';
40
41 /**
42 * Status counts.
43 *
44 * @var array
45 */
46 private $status_counts;
47
48 /**
49 * Create object instance.
50 *
51 * @since 1.5.0
52 */
53 public function __construct() {
54 do_action( 'charitable_admin_donation_post_type_start', $this );
55 }
56
57 /**
58 * Returns and/or create the single instance of this class.
59 *
60 * @since 1.5.0
61 *
62 * @return Charitable_Donation_List_Table
63 */
64 public static function get_instance() {
65 if ( is_null( self::$instance ) ) {
66 self::$instance = new self();
67 }
68
69 return self::$instance;
70 }
71
72 /**
73 * Customize donations columns.
74 *
75 * @see get_column_headers
76 *
77 * @since 1.5.0
78 *
79 * @return array
80 */
81 public function dashboard_columns() {
82 /**
83 * Filter the columns shown in the donations list table.
84 *
85 * @since 1.0.0
86 *
87 * @param array $columns The list of columns.
88 */
89 return apply_filters(
90 'charitable_donation_dashboard_column_names',
91 array(
92 'cb' => '<input type="checkbox"/>',
93 'id' => __( 'Donation', 'charitable' ),
94 'amount' => __( 'Amount Donated', 'charitable' ),
95 'campaigns' => __( 'Campaign(s)', 'charitable' ),
96 'donation_date' => __( 'Date', 'charitable' ),
97 'post_status' => __( 'Status', 'charitable' ),
98 )
99 );
100 }
101
102 /**
103 * Add information to the dashboard donations table listing.
104 *
105 * @see WP_Posts_List_Table::single_row()
106 *
107 * @since 1.5.0
108 *
109 * @param string $column_name The name of the column to display.
110 * @param int $post_id The current post ID.
111 * @return void
112 */
113 public function dashboard_column_item( $column_name, $post_id ) {
114 $donation = charitable_get_donation( $post_id );
115
116 switch ( $column_name ) {
117
118 case 'id':
119 $title = esc_attr__( 'View Donation Details', 'charitable' );
120 $name = $donation->get_donor()->get_name();
121 $email = $donation->get_donor()->get_email();
122
123 if ( $name ) {
124 $text = sprintf(
125 /* translators: %1$d is the donation ID, %2$s is the donor name */
126 _x( '#%1$d by %2$s', 'number symbol', 'charitable' ),
127 $post_id,
128 $name
129 );
130 if ( $email ) {
131 $text .= '<br/>' . sprintf(
132 /* translators: %s is the donor email */
133 _x( '%1$s', 'email', 'charitable' ), // phpcs:ignore
134 $email
135 );
136 }
137 } else {
138 $text = sprintf(
139 /* translators: %d is the donation ID */
140 _x( '#%d', 'number symbol', 'charitable' ),
141 $post_id
142 );
143 }
144
145 $url = esc_url(
146 add_query_arg(
147 array(
148 'post' => $post_id,
149 'action' => 'edit',
150 ),
151 admin_url( 'post.php' )
152 )
153 );
154
155 $display = sprintf( '<a href="%s" aria-label="%s">%s</a>', $url, $title, $text );
156 break;
157
158 case 'post_status':
159 $display = sprintf(
160 '<mark class="status %s">%s</mark>',
161 esc_attr( $donation->get_status() ),
162 strtolower( $donation->get_status_label() )
163 );
164 break;
165
166 case 'amount':
167 $display = charitable_format_money( $donation->get_total_donation_amount(), false, false, $donation->get_currency() );
168 // translators: %s is the gateway label.
169 $display .= '<span class="meta">' . sprintf( _x( 'via %s', 'gateway label in donations', 'charitable' ), $donation->get_gateway_label() ) . '</span>';
170 break;
171
172 case 'campaigns':
173 $campaigns = array();
174
175 foreach ( $donation->get_campaign_donations() as $cd ) {
176 $campaigns[] = sprintf(
177 '<a href="edit.php?post_type=%s&campaign_id=%s">%s</a>',
178 Charitable::DONATION_POST_TYPE,
179 $cd->campaign_id,
180 $cd->campaign_name
181 );
182 }
183
184 $display = implode( ', ', $campaigns );
185 break;
186
187 case 'donation_date':
188 $display = $donation->get_date();
189 if ( ! defined( 'CHARITABLE_DONATIONS_LIST_SHOW_DONATION_TIME' ) || ( defined( 'CHARITABLE_DONATIONS_LIST_SHOW_DONATION_TIME' ) && CHARITABLE_DONATIONS_LIST_SHOW_DONATION_TIME ) ) {
190 $display .= ' <span>' . $donation->get_time() . '</span>';
191 }
192 break;
193
194 default:
195 $display = '';
196 break;
197
198 }
199
200 /**
201 * Filter the output of the cell.
202 *
203 * @since 1.0.0
204 *
205 * @param string $display The content that will be displayed.
206 * @param string $column_name The name of the column.
207 * @param int $post_id The ID of the donation being shown.
208 * @param Charitable_Donation $donation The Charitable_Donation object.
209 */
210 echo apply_filters( 'charitable_donation_column_display', $display, $column_name, $post_id, $donation ); // phpcs:ignore
211 }
212
213 /**
214 * Make columns sortable.
215 *
216 * @since 1.5.0
217 *
218 * @param array $columns List of columns that are sortable by default.
219 * @return array
220 */
221 public function sortable_columns( $columns ) {
222 $sortable_columns = array(
223 'id' => 'ID',
224 'amount' => 'amount',
225 'donation_date' => 'date',
226 );
227
228 return wp_parse_args( $sortable_columns, $columns );
229 }
230
231 /**
232 * Set list table primary column for donations.
233 *
234 * Support for WordPress 4.3.
235 *
236 * @since 1.5.0
237 *
238 * @param string $default Default primary column.
239 * @param string $screen_id The current screen ID.
240 * @return string
241 */
242 public function primary_column( $default, $screen_id ) { // phpcs:ignore
243 if ( 'edit-donation' === $screen_id ) {
244 return 'id';
245 }
246
247 return $default;
248 }
249
250 /**
251 * Set row actions for donations.
252 *
253 * @since 1.5.0
254 *
255 * @param array $actions List of row actions.
256 * @param WP_Post $post The current post object.
257 * @return array
258 */
259 public function row_actions( $actions, $post ) {
260 if ( Charitable::DONATION_POST_TYPE !== $post->post_type ) {
261 return $actions;
262 }
263
264 if ( isset( $actions['inline hide-if-no-js'] ) ) {
265 unset( $actions['inline hide-if-no-js'] );
266 }
267
268 $actions['edit'] = sprintf(
269 '<a href="%s" aria-label="%s">%s</a>',
270 esc_url(
271 add_query_arg(
272 array(
273 'post' => $post->ID,
274 'action' => 'edit',
275 'show_form' => true,
276 ),
277 admin_url( 'post.php' )
278 )
279 ),
280 esc_attr__( 'Edit Donation', 'charitable' ),
281 __( 'Edit', 'charitable' )
282 );
283
284 $actions = array_merge(
285 array(
286 'view' => sprintf(
287 '<a href="%s" aria-label="%s">%s</a>',
288 esc_url(
289 add_query_arg(
290 array(
291 'post' => $post->ID,
292 'action' => 'edit',
293 ),
294 admin_url( 'post.php' )
295 )
296 ),
297 esc_attr__( 'View Details', 'charitable' ),
298 __( 'View', 'charitable' )
299 ),
300 ),
301 $actions
302 );
303
304 return $actions;
305 }
306
307 /**
308 * Customize the output of the status views.
309 *
310 * @since 1.5.0
311 *
312 * @param string[] $views The default list of status views.
313 * @return string[]
314 */
315 public function set_status_views( $views ) {
316 $counts = $this->get_status_counts();
317 $current = array_key_exists( 'post_status', $_GET ) ? esc_html( $_GET['post_status'] ) : ''; // phpcs:ignore
318
319 foreach ( charitable_get_valid_donation_statuses() as $key => $label ) {
320 $views[ $key ] = sprintf(
321 '<a href="%s"%s>%s <span class="count">(%d)</span></a>',
322 esc_url(
323 add_query_arg(
324 array(
325 'post_status' => $key,
326 'paged' => false,
327 )
328 )
329 ),
330 $current === $key ? ' class="current"' : '',
331 $label,
332 array_key_exists( $key, $counts ) ? $counts[ $key ] : '0'
333 );
334 }
335
336 $views['all'] = sprintf(
337 '<a href="%s"%s>%s <span class="count">(%d)</span></a>',
338 esc_url( remove_query_arg( array( 'post_status', 'paged' ) ) ),
339 'all' === $current || '' === $current ? ' class="current"' : '',
340 __( 'All', 'charitable' ),
341 array_sum( $counts )
342 );
343
344 unset( $views['mine'] );
345
346 return $views;
347 }
348
349 /**
350 * Add Custom bulk actions
351 *
352 * @param array $actions The list of bulk actions.
353 * @return array
354 * @since 1.5.0
355 */
356 public function custom_bulk_actions( $actions ) {
357 if ( isset( $actions['edit'] ) ) {
358 unset( $actions['edit'] );
359 }
360
361 return array_merge( $actions, $this->get_bulk_actions() );
362 }
363
364 /**
365 * Process bulk actions
366 *
367 * @param int $redirect_to The URL to redirect to.
368 * @param string $action The action being performed.
369 * @param int[] $post_ids The list of post IDs.
370 * @return string
371 * @since 1.5.0
372 */
373 public function bulk_action_handler( $redirect_to, $action, $post_ids ) {
374 /* Bail out if this is not a status-changing action. */
375 if ( strpos( $action, 'set-' ) === false ) {
376 $sendback = remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'locked', 'ids' ), wp_get_referer() );
377 wp_safe_redirect( esc_url_raw( $sendback ) );
378
379 exit();
380 }
381
382 $donation_statuses = charitable_get_valid_donation_statuses();
383
384 $new_status = str_replace( 'set-', '', $action ); // get the status name from action.
385 $report_action = 'bulk_' . Charitable::DONATION_POST_TYPE . '_status_update';
386
387 /**
388 * Sanity check: bail out if this is actually not a status, or is
389 * not a registered status.
390 */
391 if ( ! isset( $donation_statuses[ $new_status ] ) ) {
392 return $redirect_to;
393 }
394
395 foreach ( $post_ids as $post_id ) {
396 $donation = charitable_get_donation( $post_id );
397 $donation->update_status( $new_status );
398
399 do_action( 'charitable_donations_table_do_bulk_action', $post_id, $new_status );
400 }
401
402 $redirect_to = add_query_arg( $report_action, count( $post_ids ), $redirect_to );
403
404 return $redirect_to;
405 }
406
407
408 /**
409 * Remove edit from the bulk actions.
410 *
411 * @since 1.5.0
412 *
413 * @param array $actions List of bulk actions.
414 * @return array
415 */
416 public function remove_bulk_actions( $actions ) {
417 if ( isset( $actions['edit'] ) ) {
418 unset( $actions['edit'] );
419 }
420
421 return $actions;
422 }
423
424 /**
425 * Retrieve the bulk actions.
426 *
427 * @since 1.5.0
428 *
429 * @return array $actions Array of the bulk actions
430 */
431 public function get_bulk_actions() {
432 $actions = array();
433
434 foreach ( charitable_get_valid_donation_statuses() as $status_key => $label ) {
435 /* translators: %s is the status label */
436 $actions[ 'set-' . $status_key ] = sprintf( _x( 'Set to %s', 'set donation status to x', 'charitable' ), $label );
437 }
438
439 /**
440 * Filter the list of bulk actions for donations.
441 *
442 * @since 1.4.0
443 *
444 * @param array $actions The list of bulk actions.
445 */
446 return apply_filters( 'charitable_donations_table_bulk_actions', $actions );
447 }
448
449 /**
450 * Add extra bulk action options to mark orders as complete or processing.
451 *
452 * Using Javascript until WordPress core fixes: https://core.trac.wordpress.org/ticket/16031
453 *
454 * @since 1.5.0
455 *
456 * @global string $post_type
457 *
458 * @return void
459 */
460 public function bulk_admin_footer() {
461 global $post_type;
462
463 if ( Charitable::DONATION_POST_TYPE === $post_type ) {
464
465 $js = '<script type="text/javascript">';
466 $js .= '(function($) {';
467 foreach ( $this->get_bulk_actions() as $status_key => $label ) {
468 $js .= sprintf( "jQuery('<option>').val('%s').text('%s').appendTo( [ '#bulk-action-selector-top', '#bulk-action-selector-bottom' ] );", $status_key, $label );
469 }
470 $js .= '})(jQuery);';
471 $js .= '</script>';
472
473 echo $js; // phpcs:ignore
474 }
475 }
476
477 /**
478 * Process the new bulk actions for changing order status.
479 *
480 * @since 1.5.0
481 *
482 * @return void
483 */
484 public function process_bulk_action() {
485 /* We only want to deal with donations. In case any other CPTs have an 'active' action. */
486 if ( ! isset( $_REQUEST['post_type'] ) || Charitable::DONATION_POST_TYPE !== $_REQUEST['post_type'] || ! isset( $_REQUEST['post'] ) ) {
487 return;
488 }
489
490 check_admin_referer( 'bulk-posts' );
491
492 /* Get the action. */
493 $action = '';
494
495 if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] ) {
496 $action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) );
497 } elseif ( isset( $_REQUEST['action2'] ) && -1 != $_REQUEST['action2'] ) {
498 $action = sanitize_text_field( wp_unslash( $_REQUEST['action2'] ) );
499 }
500
501 $post_ids = array_map( 'absint', (array) $_REQUEST['post'] );
502 $redirect_to = add_query_arg(
503 array(
504 'post_type' => Charitable::DONATION_POST_TYPE,
505 ),
506 admin_url( 'edit.php' )
507 );
508 $redirect_to = $this->bulk_action_handler( $redirect_to, $action, $post_ids );
509
510 wp_safe_redirect( esc_url_raw( $redirect_to ) );
511
512 exit();
513 }
514
515 /**
516 * Show confirmation message that order status changed for number of orders.
517 *
518 * @since 1.5.0
519 *
520 * @global string $post_type
521 * @global string $pagenow
522 *
523 * @return void
524 */
525 public function bulk_admin_notices() {
526 global $post_type, $pagenow;
527
528 /* Bail out if not on shop order list page. */
529 if ( 'edit.php' !== $pagenow || Charitable::DONATION_POST_TYPE !== $post_type ) {
530 return;
531 }
532
533 /* Check if any status changes happened. */
534 $report_action = 'bulk_' . Charitable::DONATION_POST_TYPE . '_status_update';
535
536 if ( ! empty( $_REQUEST[ $report_action ] ) ) { // phpcs:ignore
537 $number = absint( $_REQUEST[ $report_action ] ); // phpcs:ignore
538 // translators: %s is the number of orders.
539 $message = sprintf( _n( '%s donation status changed.', '%s donation statuses changed.', $number, 'charitable' ), number_format_i18n( $number ) );
540 echo '<div class="updated"><p>' . esc_html( $message ) . '</p></div>';
541 }
542 }
543
544 /**
545 * Change messages when a post type is updated.
546 *
547 * @since 1.5.0
548 * @version 1.8.1.15
549 *
550 * @global WP_Post $post
551 * @global int $post_ID
552 * @param array $messages The default list of messages.
553 * @return array
554 */
555 public function post_messages( $messages ) {
556 global $post, $post_ID;
557
558 $messages[ Charitable::DONATION_POST_TYPE ] = array(
559 0 => '', // Unused. Messages start at index 1.
560 // translators: %s: the URL.
561 1 => sprintf( __( 'Donation updated. <a href="%s">View Donation</a>', 'charitable' ), esc_url( get_permalink( $post_ID ) ) ),
562 2 => __( 'Custom field updated.', 'charitable' ),
563 3 => __( 'Custom field deleted.', 'charitable' ),
564 4 => __( 'Donation updated.', 'charitable' ),
565 // translators: %s: post title.
566 5 => isset( $_GET['revision'] ) ? sprintf( __( 'Donation restored to revision from %s', 'charitable' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
567 // translators: %s: the URL.
568 6 => sprintf( __( 'Donation published. <a href="%s">View Donation</a>', 'charitable' ), esc_url( get_permalink( $post_ID ) ) ),
569 7 => __( 'Donation saved.', 'charitable' ),
570 8 => sprintf(
571 // translators: %s: the URL.
572 __( 'Donation submitted. <a target="_blank" href="%s">Preview Donation</a>', 'charitable' ),
573 esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) )
574 ),
575 9 => sprintf(
576 // translators: %1$s is the date, %2$s is the preview link.
577 __( 'Donation scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview Donation</a>', 'charitable' ),
578 date_i18n( 'M j, Y @ G:i', strtotime( $post->post_date ) ),
579 esc_url( get_permalink( $post_ID ) )
580 ),
581 10 => sprintf(
582 // translators: %s is the preview link.
583 __( 'Donation draft updated. <a target="_blank" href="%s">Preview Donation</a>', 'charitable' ),
584 esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) )
585 ),
586 11 => __( 'Donation updated and email sent.', 'charitable' ),
587 12 => __( 'Email could not be sent.', 'charitable' ),
588 );
589
590 return $messages;
591 }
592
593 /**
594 * Modify bulk messages
595 *
596 * @since 1.5.0
597 *
598 * @param array $bulk_messages Bulk messages.
599 * @param array $bulk_counts Bulk counts.
600 * @return array
601 */
602 public function bulk_messages( $bulk_messages, $bulk_counts ) {
603 $bulk_messages[ Charitable::DONATION_POST_TYPE ] = array(
604 // translators: %d: number of items.
605 'updated' => _n( '%d donation updated.', '%d donations updated.', $bulk_counts['updated'], 'charitable' ),
606 'locked' => ( 1 == $bulk_counts['locked'] ) ? __( '1 donation not updated, somebody is editing it.', 'charitable' ) :
607 // translators: %d: number of items.
608 _n( '%s donation not updated, somebody is editing it.', '%s donations not updated, somebody is editing them.', $bulk_counts['locked'], 'charitable' ),
609 // translators: %s: number of items.
610 'deleted' => _n( '%s donation permanently deleted.', '%s donations permanently deleted.', $bulk_counts['deleted'], 'charitable' ),
611 // translators: %s: number of items.
612 'trashed' => _n( '%s donation moved to the Trash.', '%s donations moved to the Trash.', $bulk_counts['trashed'], 'charitable' ),
613 // translators: %s: number of items.
614 'untrashed' => _n( '%s donation restored from the Trash.', '%s donations restored from the Trash.', $bulk_counts['untrashed'], 'charitable' ),
615 );
616
617 return $bulk_messages;
618 }
619
620 /**
621 * Disable the month's dropdown (will replace with custom range search).
622 *
623 * @since 1.5.0
624 *
625 * @param mixed $disable Whether to disable the dropdown.
626 * @param string $post_type The current post type.
627 * @return boolean
628 */
629 public function disable_months_dropdown( $disable, $post_type ) {
630 if ( Charitable::DONATION_POST_TYPE == $post_type ) {
631 $disable = true;
632 }
633
634 return $disable;
635 }
636
637 /**
638 * Add date-based filters above the donations table.
639 *
640 * @since 1.5.0
641 *
642 * @global string $typenow The current post type.
643 *
644 * @return void
645 */
646 public function add_filters() {
647 global $typenow;
648
649 /* Show custom filters to filter orders by donor. */
650 if ( in_array( $typenow, array( Charitable::DONATION_POST_TYPE ) ) ) {
651 charitable_admin_view( 'donations-page/filters' );
652 }
653 }
654
655 /**
656 * Add extra buttons after filters
657 *
658 * @since 1.5.0
659 *
660 * @global string $typenow The current post type.
661 *
662 * @param string $which The context where this is being called.
663 * @return void
664 */
665 public function add_export( $which ) {
666 global $typenow;
667
668 if ( ! current_user_can( 'export_charitable_reports' ) ) {
669 return;
670 }
671
672 /* Add the export button. */
673 if ( 'top' == $which && in_array( $typenow, array( Charitable::DONATION_POST_TYPE ) ) ) {
674 charitable_admin_view( 'donations-page/export' );
675 }
676 }
677
678 /**
679 * Add modal template to footer.
680 *
681 * @since 1.5.0
682 *
683 * @global string $typenow The current post type.
684 * @global string $pagenow The current page.
685 *
686 * @return void
687 */
688 public function modal_forms() {
689 global $typenow, $pagenow;
690
691 /* Add the modal form. */
692 if ( 'edit.php' === $pagenow && in_array( $typenow, array( Charitable::DONATION_POST_TYPE ), true ) ) {
693 charitable_admin_view( 'donations-page/export-form' );
694 charitable_admin_view( 'donations-page/filter-form' );
695 }
696 }
697
698 /**
699 * Admin scripts and styles.
700 *
701 * Set up the scripts & styles used for the modal.
702 *
703 * @since 1.5.0
704 *
705 * @global string $typenow The current post type.
706 *
707 * @param string $hook The current page hook.
708 * @return void
709 */
710 public function load_scripts( $hook ) {
711 if ( 'edit.php' !== $hook ) {
712 return;
713 }
714
715 global $typenow;
716
717 /* Enqueue the scripts for donation page */
718 if ( in_array( $typenow, array( Charitable::DONATION_POST_TYPE ), true ) ) {
719 wp_enqueue_style( 'lean-modal-css' );
720 wp_enqueue_script( 'jquery' );
721 wp_enqueue_script( 'lean-modal' );
722 wp_enqueue_script( 'charitable-admin-tables' );
723 }
724 }
725
726 /**
727 * Add custom filters to the query that returns the donations to be displayed.
728 *
729 * @since 1.5.0
730 *
731 * @global string $typenow The current post type.
732 * @global string $pagenow The current admin page.
733 *
734 * @param array $vars The array of args to pass to WP_Query.
735 * @return array
736 */
737 public function filter_request_query( $vars ) {
738 global $typenow, $pagenow;
739
740 if ( 'edit.php' !== $pagenow || Charitable::DONATION_POST_TYPE !== $typenow ) {
741 return $vars;
742 }
743
744 /* No Status: fix WP's crappy handling of "all" post status. */
745 if ( ! isset( $vars['post_status'] ) || empty( $vars['post_status'] ) ) {
746 $vars['post_status'] = array_keys( charitable_get_valid_donation_statuses() );
747 }
748
749 // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
750 if ( isset( $_GET['charitable_nonce'] ) && wp_verify_nonce( wp_unslash( $_GET['charitable_nonce'] ), 'charitable_filter_campaigns' ) ) :
751
752 /* Set up date query */
753 if ( isset( $_GET['start_date'] ) && ! empty( $_GET['start_date'] ) ) {
754 $start_date = $this->get_parsed_date( sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) );
755 $vars['date_query']['after'] = array(
756 'year' => $start_date['year'],
757 'month' => $start_date['month'],
758 'day' => $start_date['day'],
759 );
760 }
761
762 if ( isset( $_GET['end_date'] ) && ! empty( $_GET['end_date'] ) ) {
763 $end_date = $this->get_parsed_date( sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) );
764 $vars['date_query']['before'] = array(
765 'year' => $end_date['year'],
766 'month' => $end_date['month'],
767 'day' => $end_date['day'],
768 );
769 }
770
771 if ( isset( $vars['date_query'] ) ) {
772 $vars['date_query']['inclusive'] = true;
773 }
774
775 endif;
776
777 /* Filter by campaign. */
778 if ( isset( $_GET['campaign_id'] ) && 'all' !== $_GET['campaign_id'] ) {
779 $vars['post__in'] = charitable_get_table( 'campaign_donations' )->get_donation_ids_for_campaign( (int) sanitize_text_field( wp_unslash( $_GET['campaign_id'] ) ) );
780 }
781
782 /* If the user cannot view/edit others donations, filter by author. */
783 if ( ! current_user_can( 'edit_others_donations' ) ) {
784 $vars['author'] = get_current_user_id();
785 }
786
787 return $vars;
788 }
789
790 /**
791 * Column sorting handler.
792 *
793 * @since 1.5.0
794 *
795 * @global string $typenow The current post type.
796 * @global WPDB $wpdb The WPDB object.
797 * @param array $clauses Array of SQL query clauses.
798 * @return array
799 */
800 public function sort_donations( $clauses ) {
801 global $typenow, $wpdb;
802
803 if ( Charitable::DONATION_POST_TYPE != $typenow ) {
804 return $clauses;
805 }
806
807 if ( ! isset( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
808 return $clauses;
809 }
810
811 /* Sorting */
812 $order = isset( $_GET['order'] ) && strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) == 'ASC' ? 'ASC' : 'DESC'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
813
814 switch ( sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
815 case 'amount':
816 $clauses['join'] = "JOIN {$wpdb->prefix}charitable_campaign_donations cd ON cd.donation_id = $wpdb->posts.ID ";
817 $clauses['orderby'] = 'cd.amount ' . $order;
818 break;
819 }
820
821 return $clauses;
822 }
823
824 /**
825 * Return the status counts, taking into account any current filters.
826 *
827 * @since 1.5.0
828 *
829 * @return array
830 */
831 protected function get_status_counts() {
832 if ( ! isset( $this->status_counts ) ) {
833
834 $args = array();
835
836 // phpcs:disable WordPress.Security.NonceVerification.Recommended
837 if ( isset( $_GET['s'] ) ) {
838 $s_value = sanitize_text_field( wp_unslash( $_GET['s'] ) );
839 if ( strlen( $s_value ) ) {
840 $args['s'] = $s_value;
841 }
842 }
843
844 if ( isset( $_GET['start_date'] ) ) {
845 $start_date_value = sanitize_text_field( wp_unslash( $_GET['start_date'] ) );
846 if ( strlen( $start_date_value ) ) {
847 $args['start_date'] = $this->get_parsed_date( $start_date_value );
848 }
849 }
850
851 if ( isset( $_GET['end_date'] ) ) {
852 $end_date_value = sanitize_text_field( wp_unslash( $_GET['end_date'] ) );
853 if ( strlen( $end_date_value ) ) {
854 $args['end_date'] = $this->get_parsed_date( $end_date_value );
855 }
856 }
857 // phpcs:enable WordPress.Security.NonceVerification.Recommended
858
859 /* If the user cannot view/edit others donations, filter by author. */
860 if ( ! current_user_can( 'edit_others_donations' ) ) {
861 $args['author'] = get_current_user_id();
862 }
863
864 $status_counts = Charitable_Donations::count_by_status( $args );
865
866 foreach ( charitable_get_valid_donation_statuses() as $key => $label ) {
867 $this->status_counts[ $key ] = array_key_exists( $key, $status_counts )
868 ? $status_counts[ $key ]->num_donations
869 : 0;
870 }
871 }
872
873 return $this->status_counts;
874 }
875
876 /**
877 * Given a date, returns an array containing the date, month and year.
878 *
879 * @since 1.5.0
880 *
881 * @param string $date A date as a string that can be parsed by strtotime.
882 * @return string[]
883 */
884 protected function get_parsed_date( $date ) {
885 $time = charitable_sanitize_date_alt_format( $date );
886
887 return array(
888 'year' => date( 'Y', $time ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
889 'month' => date( 'm', $time ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
890 'day' => date( 'd', $time ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
891 );
892 }
893
894 /**
895 * Show blank slate.
896 *
897 * @since 1.8.1.5
898 * @version 1.8.8.6, 1.8.9.1, 1.8.10
899 *
900 * @param string $which String which tablenav is being shown.
901 *
902 * @return void
903 */
904 public function maybe_render_blank_state( $which = '' ) {
905 global $post_type, $typenow;
906
907 // Static flag to prevent double rendering
908 static $blank_state_rendered = false;
909
910 // Use $typenow as fallback if $post_type is not set
911 $current_post_type = $post_type ? $post_type : $typenow;
912
913 // Only proceed if we're on the correct post type
914 if ( $current_post_type !== $this->list_table_type ) {
915 return;
916 }
917
918 // Check post count
919 $counts = (array) wp_count_posts( $current_post_type );
920 unset( $counts['auto-draft'] );
921 $count = array_sum( $counts );
922
923 // Skip if there are posts or blank slate is disabled
924 if ( ! defined( 'CHARITABLE_FORCE_BLANK_SLATE' ) || ! CHARITABLE_FORCE_BLANK_SLATE ) {
925 if ( 0 < $count || ( defined( 'CHARITABLE_DISABLE_BLANK_SLATE' ) && CHARITABLE_DISABLE_BLANK_SLATE ) ) {
926 return;
927 }
928 }
929
930 // Prefer bottom, but render in top if bottom doesn't fire (when there are 0 posts)
931 if ( 'bottom' === $which && ! $blank_state_rendered ) {
932 $blank_state_rendered = true;
933 $this->enqueue_blank_slate_assets();
934 $this->render_blank_state();
935 echo '<style type="text/css">#posts-filter .wp-list-table, #posts-filter .tablenav.top, .tablenav.bottom .actions, .wrap .subsubsub, .wrap .search-box, .wrap .tablenav-pages { display: none; } #posts-filter .tablenav.bottom { height: auto; } body.post-type-donation .page-title-action { display: none; } </style>';
936 } elseif ( 'top' === $which && ! $blank_state_rendered ) {
937 // Fallback: if bottom never fires (WordPress doesn't render it with 0 posts), render in top
938 $blank_state_rendered = true;
939 $this->enqueue_blank_slate_assets();
940 $this->render_blank_state();
941 echo '<style type="text/css">#posts-filter .wp-list-table, #posts-filter .tablenav.top .actions, .wrap .subsubsub, .wrap .search-box, .wrap .tablenav-pages { display: none; } #posts-filter .tablenav.top { height: auto; } body.post-type-donation .page-title-action { display: none; } </style>';
942 }
943 }
944
945 /**
946 * Renders advice in the event that no donations exist yet.
947 *
948 * @since 1.8.1.5
949 * @version 1.8.10
950 *
951 * @return void
952 */
953 public function render_blank_state(): void {
954 ?>
955 <div class="charitable-blank-slate">
956 <!-- Page Header -->
957 <div class="page-header">
958 <h1 class="page-title"><?php esc_html_e( 'Donations', 'charitable' ); ?></h1>
959 <a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=donation' ) ); ?>" class="btn-primary"><?php esc_html_e( '+ Add Manual Donation', 'charitable' ); ?></a>
960 </div>
961
962 <!-- Primary Welcome Section -->
963 <div class="welcome-card">
964 <h2 class="welcome-title"><?php esc_html_e( 'Welcome to Charitable!', 'charitable' ); ?></h2>
965 <p class="welcome-description">
966 <?php esc_html_e( 'Start accepting online donations by setting up payment processing for your campaigns.', 'charitable' ); ?>
967 </p>
968 <a href="<?php echo esc_url( admin_url( 'admin.php?page=charitable-settings&tab=gateways' ) ); ?>" class="btn-primary btn-large"><?php esc_html_e( 'Set Up Payment Processing', 'charitable' ); ?></a>
969 </div>
970
971 <!-- Secondary Actions -->
972 <div class="secondary-actions">
973 <div class="action-card">
974 <h3><?php esc_html_e( 'Already Have Donations Elsewhere?', 'charitable' ); ?></h3>
975 <?php
976 $givewp_installed = $this->is_givewp_installed();
977 $givewp_donation_count = $givewp_installed ? $this->get_givewp_donation_count() : 0;
978
979 if ( $givewp_installed && $givewp_donation_count > 0 ) :
980 ?>
981 <p class="givewp-detection-text">
982 <?php
983 printf(
984 wp_kses(
985 /* translators: 1: bolded "GiveWP", 2: number of donations. */
986 __( 'We detected %1$s with %2$s donations installed on your site. Use our import tool to import them into Charitable.', 'charitable' ),
987 [
988 'strong' => [],
989 'span' => [ 'class' => [] ],
990 ]
991 ),
992 '<strong>GiveWP</strong>',
993 '<span class="givewp-count">' . absint( $givewp_donation_count ) . '</span>'
994 );
995 ?>
996 </p>
997 <a href="https://www.wpcharitable.com/documentation/import-export-tool-givewp-to-charitable/" class="btn-secondary" target="_blank"><?php esc_html_e( 'Import GiveWP Donations', 'charitable' ); ?></a>
998 <?php elseif ( $givewp_installed && 0 === $givewp_donation_count ) : ?>
999 <p class="givewp-detection-text">
1000 <?php
1001 printf(
1002 wp_kses(
1003 /* translators: %s: bolded "GiveWP". */
1004 __( 'Did you know you can import %s donations, campaigns, and donors right into Charitable?', 'charitable' ),
1005 [ 'strong' => [] ]
1006 ),
1007 '<strong>GiveWP</strong>'
1008 );
1009 ?>
1010 </p>
1011 <a href="<?php echo esc_url( admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=givewp' ) ); ?>" class="btn-secondary"><?php esc_html_e( 'Import Tools', 'charitable' ); ?></a>
1012 <?php else : ?>
1013 <p><?php esc_html_e( 'Import your existing donations from your old fundraising platform to get started quickly with Charitable.', 'charitable' ); ?></p>
1014 <a href="<?php echo esc_url( admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable' ) ); ?>" class="btn-secondary"><?php esc_html_e( 'Import Donations', 'charitable' ); ?></a>
1015 <?php endif; ?>
1016 </div>
1017
1018 <div class="blank-slate-feature-card">
1019 <div class="blank-slate-feature-title-icon-row">
1020 <div class="blank-slate-feature-icon">
1021 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) . 'images/icons/recurring.svg' ); ?>" alt="<?php esc_attr_e( 'Recurring Donations', 'charitable' ); ?>" width="32" height="32" />
1022 </div>
1023 <h3 class="blank-slate-feature-title"><?php esc_html_e( 'Recurring Donations', 'charitable' ); ?> <span class="blank-slate-pro-badge"><?php esc_html_e( 'PRO', 'charitable' ); ?></span></h3>
1024 </div>
1025 <div class="blank-slate-feature-content">
1026 <p class="blank-slate-feature-description"><?php esc_html_e( 'Increase donor lifetime value by allowing supporters to set up recurring monthly donations.', 'charitable' ); ?></p>
1027 <div class="blank-slate-button-row">
1028 <?php
1029 $recurring_state = $this->get_recurring_addon_state();
1030
1031 if ( 'upgrade' === $recurring_state['button_action'] ) {
1032 $upgrade_url = isset( $recurring_state['upgrade_url'] ) ? $recurring_state['upgrade_url'] : '#';
1033 ?>
1034 <a href="<?php echo esc_url( $upgrade_url ); ?>" class="charitable-dashboard-v2-enhance-grid-button <?php echo esc_attr( $recurring_state['button_class'] ); ?>" data-action="upgrade" target="_blank"><?php echo esc_html( $recurring_state['button_text'] ); ?></a>
1035 <?php
1036 } else {
1037 $button_attributes = [
1038 'data-action="' . esc_attr( $recurring_state['button_action'] ) . '"',
1039 'data-slug="charitable-recurring-donations"',
1040 'data-type="charitable_addon"',
1041 ];
1042
1043 if ( $recurring_state['button_disabled'] ) {
1044 $button_attributes[] = 'disabled';
1045 }
1046 ?>
1047 <button class="charitable-dashboard-v2-enhance-grid-button <?php echo esc_attr( $recurring_state['button_class'] ); ?>" <?php echo implode( ' ', $button_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php echo esc_html( $recurring_state['button_text'] ); ?></button>
1048 <?php
1049 }
1050 ?>
1051 <a href="https://www.wpcharitable.com/extensions/charitable-recurring-donations/" class="charitable-dashboard-v2-enhance-grid-button charitable-dashboard-v2-learn-more-button" target="_blank"><?php esc_html_e( 'Learn More', 'charitable' ); ?></a>
1052 </div>
1053 </div>
1054 </div>
1055 </div>
1056
1057 <!-- Help Resources -->
1058 <div class="help-resources">
1059 <h3><?php esc_html_e( 'Additional Resources', 'charitable' ); ?></h3>
1060 <ul class="help-links">
1061 <li><a href="https://www.wpcharitable.com/documentation/adding-payment-gateways/?utm_campaign=liteplugin&utm_source=charitableplugin&utm_medium=donation-page&utm_content=Payment%20Guide" target="_blank"><?php esc_html_e( 'Setting Up Payment Gateways', 'charitable' ); ?></a></li>
1062 <li><a href="https://www.wpcharitable.com/documentation/import-export-tool-givewp-to-charitable/?utm_campaign=liteplugin&utm_source=charitableplugin&utm_medium=donation-page&utm_content=Import%20Guide" target="_blank"><?php esc_html_e( 'Import Donations from GiveWP', 'charitable' ); ?></a></li>
1063 <li><a href="https://www.wpcharitable.com/features-main/reports/?utm_campaign=liteplugin&utm_source=charitableplugin&utm_medium=donation-page&utm_content=Reports%20Guide" target="_blank"><?php esc_html_e( 'Donation Reports & Analytics', 'charitable' ); ?></a></li>
1064 </ul>
1065 </div>
1066 </div>
1067 <?php
1068 }
1069
1070
1071 /**
1072 * Enqueue dashboard assets for AJAX button functionality on blank slate.
1073 *
1074 * @since 1.8.10
1075 *
1076 * @return void
1077 */
1078 private function enqueue_blank_slate_assets() {
1079 $version = charitable()->get_version();
1080 $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
1081
1082 wp_enqueue_script(
1083 'charitable-admin-dashboard',
1084 charitable()->get_path( 'assets', false ) . 'js/admin/charitable-admin-dashboard' . $suffix . '.js',
1085 [ 'jquery' ],
1086 $version,
1087 true
1088 );
1089
1090 wp_localize_script(
1091 'charitable-admin-dashboard',
1092 'charitable_admin',
1093 [
1094 'nonce' => wp_create_nonce( 'charitable-admin' ),
1095 'ajaxurl' => admin_url( 'admin-ajax.php' ),
1096 ]
1097 );
1098 }
1099
1100 /**
1101 * Check if GiveWP is installed and active.
1102 *
1103 * @since 1.8.10
1104 *
1105 * @return bool
1106 */
1107 private function is_givewp_installed() {
1108 if ( ! function_exists( 'is_plugin_active' ) ) {
1109 include_once ABSPATH . 'wp-admin/includes/plugin.php';
1110 }
1111
1112 return is_plugin_active( 'give/give.php' );
1113 }
1114
1115 /**
1116 * Count GiveWP donations (payments) in live mode.
1117 *
1118 * @since 1.8.10
1119 *
1120 * @return int
1121 */
1122 private function get_givewp_donation_count() {
1123 global $wpdb;
1124
1125 $table_name = $wpdb->prefix . 'give_donationmeta';
1126
1127 // Check if the GiveWP donation meta table exists.
1128 if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) !== $table_name ) {
1129 return 0;
1130 }
1131
1132 return (int) $wpdb->get_var(
1133 $wpdb->prepare(
1134 "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
1135 INNER JOIN {$wpdb->prefix}give_donationmeta dm ON p.ID = dm.donation_id
1136 WHERE p.post_type = 'give_payment'
1137 AND p.post_status IN ('publish', 'pending', 'failed', 'cancelled', 'refunded', 'revoked', 'abandoned', 'give_subscription')
1138 AND dm.meta_key = '_give_payment_mode'
1139 AND dm.meta_value = %s",
1140 'live'
1141 )
1142 );
1143 }
1144
1145 /**
1146 * Get the Recurring Donations addon button state based on install/license status.
1147 *
1148 * @since 1.8.10
1149 *
1150 * @return array
1151 */
1152 private function get_recurring_addon_state() {
1153 $slug = 'charitable-recurring';
1154
1155 if ( ! function_exists( 'is_plugin_active' ) ) {
1156 include_once ABSPATH . 'wp-admin/includes/plugin.php';
1157 }
1158
1159 $plugin_file = $slug . '/' . $slug . '.php';
1160 $is_installed = file_exists( WP_PLUGIN_DIR . '/' . $plugin_file );
1161 $is_active = $is_installed && is_plugin_active( $plugin_file );
1162 $has_required_plan = $this->has_required_plan_for_recurring();
1163
1164 if ( ! $has_required_plan ) {
1165 return [
1166 'button_text' => __( 'Upgrade To Pro', 'charitable' ),
1167 'button_class' => 'charitable-dashboard-v2-upgrade-button',
1168 'button_action' => 'upgrade',
1169 'button_disabled' => false,
1170 'upgrade_url' => 'https://wpcharitable.com/lite-upgrade/?discount=LITEUPGRADE&utm_source=WordPress&utm_campaign=liteplugin&utm_medium=blank-slate&utm_content=RecurringDonations',
1171 ];
1172 }
1173
1174 if ( $is_installed && $is_active ) {
1175 return [
1176 'button_text' => __( 'Installed', 'charitable' ),
1177 'button_class' => 'charitable-dashboard-v2-installed-button',
1178 'button_action' => 'installed',
1179 'button_disabled' => true,
1180 ];
1181 }
1182
1183 if ( $is_installed && ! $is_active ) {
1184 return [
1185 'button_text' => __( 'Activate', 'charitable' ),
1186 'button_class' => 'charitable-dashboard-v2-activate-button',
1187 'button_action' => 'activate_addon',
1188 'button_disabled' => false,
1189 ];
1190 }
1191
1192 return [
1193 'button_text' => __( 'Install & Activate', 'charitable' ),
1194 'button_class' => 'charitable-dashboard-v2-install-button',
1195 'button_action' => 'install_addon',
1196 'button_disabled' => false,
1197 ];
1198 }
1199
1200 /**
1201 * Check if the user's plan includes Recurring Donations.
1202 *
1203 * @since 1.8.10
1204 *
1205 * @return bool
1206 */
1207 private function has_required_plan_for_recurring() {
1208 $current_plan = Charitable_Addons_Directory::get_current_plan_slug();
1209 $allowed_plans = [ 'plus', 'pro', 'agency' ];
1210
1211 return in_array( $current_plan, $allowed_plans, true );
1212 }
1213 }
1214
1215 endif;
1216