date_filter_values = apply_filters( 'charitable_dashboard_date_filter_values', array( '7' => __( 'Last 7 Days', 'charitable' ), '14' => __( 'Last 14 Days', 'charitable' ), '30' => __( 'Last 30 Days', 'charitable' ), '0' => __( 'Custom Date Range', 'charitable' ), ) ); $this->report_type = $report_type; $this->start_date = $start_date; $this->end_date = $end_date; $this->status = $status; $this->campaign_id = $campaign_id; $this->category_id = $category_id; // Customize how we grab data from the database based on the report type. if ( $this->is_report_type_donor( $report_type ) ) { $this->get_donors_data( $report_type, $start_date, $end_date, $campaign_id, $category_id ); } else { $this->get_data( $report_type, $start_date, $end_date, $status, $campaign_id, $category_id ); } $this->maybe_load_scripts(); $this->maybe_add_reports_cta(); do_action( 'charitable_admin_reports_init_end', $this ); } /** * Run things upon init. * * @since 1.8.1 * * @param string $report_type The report type. * @param array $args The report args. * * @return void */ public function init_with_array( $report_type = 'overview', $args = array() ) { do_action( 'charitable_admin_reports_init_start', $this ); // Get the date filter values. $this->date_filter_values = apply_filters( 'charitable_dashboard_date_filter_values', array( '7' => __( 'Last 7 Days', 'charitable' ), '14' => __( 'Last 14 Days', 'charitable' ), '30' => __( 'Last 30 Days', 'charitable' ), '0' => __( 'Custom Date Range', 'charitable' ), ) ); $defaults = array( 'report_type' => 'overview', 'start_date' => false, 'end_date' => false, 'status' => false, 'campaign_id' => false, 'category_id' => false, 'limit' => false, 'offset' => false, 'ppage' => false, ); $args = wp_parse_args( $args, $defaults ); $this->report_type = $args['report_type']; $this->start_date = $args['start_date']; $this->end_date = $args['end_date']; $this->status = $args['status']; $this->campaign_id = $args['campaign_id']; $this->category_id = $args['category_id']; // Customize how we grab data from the database based on the report type. if ( $this->is_report_type_donor( $report_type ) ) { // override the report type in $args with what was passed in. $args['report_type'] = $report_type; $this->get_donors_data_array( $report_type, $args ); } else { $this->get_data( $report_type, $args ); } $this->maybe_load_scripts(); $this->maybe_add_reports_cta(); do_action( 'charitable_admin_reports_init_end', $this ); } /** * Generate HTML for the Top Campaigns list. * * @since 1.8.1 * @version 1.8.3 Make status translatable. * * @param object $top_campaigns The top campaigns data. * @param bool $include_icons Whether to include icons. * * @return string */ public function generate_top_campaigns( $top_campaigns = false, $include_icons = true ) { if ( empty( $top_campaigns ) || empty( $top_campaigns->posts ) ) { return; } ob_start(); foreach ( $top_campaigns->posts as $campaign ) : $_campaign = $campaign->ID ? charitable_get_campaign( $campaign->ID ) : false; $status = $_campaign ? $_campaign->get_status() : false; switch ( $status ) { case 'active': $status_string = esc_html__( 'Active', 'charitable' ); break; case 'draft': $status_string = esc_html__( 'Draft', 'charitable' ); break; case 'completed': $status_string = esc_html__( 'Completed', 'charitable' ); break; case 'failed': $status_string = esc_html__( 'Failed', 'charitable' ); break; default: $status_string = ucwords( $status ); break; } $donated_amount = $_campaign->get_donated_amount(); $donor_count = $_campaign->get_donor_count(); $donation_goal = $_campaign->get_goal(); $time_ended = $_campaign->get_time_since_ended(); $end_date = $_campaign->get_end_date(); $percent_donated = $_campaign->get_percent_donated_raw(); if ( $percent_donated > 100 ) { $percent_donated = 100; } ?>

  • 0 && $donation_goal && $percent_donated > 0 ) : ?>

  • = $limit ) { break; } $donation_id = intval( $donation->donation_id ); $donor_name = esc_html( $donation->first_name ) . ' ' . esc_html( $donation->last_name ); $payment_method = esc_html( $donation->payment_gateway ); $donation_status = esc_html( $donation->donation_status ); $donation_status_label = array_key_exists( $donation_status, $statuses ) ? $statuses[ $donation_status ] : $donation_status; ?>
  • #

    : amount ); // phpcs:ignore ?>

    :

  • . * * @return string */ public function get_activity_report_filter_dropdown( $selected_values = array() ) { if ( ! is_array( $selected_values ) ) { $selected_values = array( $selected_values ); } $options_values = apply_filters( 'charitable_dashboard_activity_report_type_filter_values', array( 'donations-made' => __( 'Donations Made (All)', 'charitable' ), 'donations-made-paid' => __( 'Donations Made (Paid)', 'charitable' ), 'refunds' => __( 'Refunds', 'charitable' ), 'campaigns-created' => __( 'Campaigns Created', 'charitable' ), 'user-comments' => __( 'User Comments', 'charitable' ), ) ); ob_start(); ?>

    type ) ) { return; } switch ( $activity->type ) { case 'donation': return $this->get_donation_activity_secondary_info( $activity ); case 'campaign': return $this->get_campaign_activity_secondary_info( $activity ); default: return; } } /** * Generate HTML for the "secondary" info for a donation activity. * * @since 1.8.1 * * @param object $activity The activity data. * * @return string */ public function get_donation_activity_secondary_info( $activity ) { if ( empty( $activity ) || empty( $activity->type ) ) { return; } $secondary_info = ''; $primary_action = isset( $activity->primary_action ) ? $activity->primary_action : false; switch ( $primary_action ) { case 'charitable-completed': case 'charitable-pending': case 'charitable-refunded': case 'charitable-failed': $admin_campaign_url = ! empty( $activity->campaign_id ) ? charitable_get_admin_campaign_edit_url( $activity->campaign_id ) : '#'; $admin_donation_url = ! empty( $activity->item_id ) ? charitable_get_admin_donation_edit_url( $activity->item_id ) : false; $campaign_title = ! empty( $activity->campaign_title ) ? ' to ' . esc_html( $activity->campaign_title ) . ' ' : ''; $secondary_info = $admin_donation_url ? '

    ' . charitable_format_money( $activity->amount, 2, true ) . '' . $campaign_title . '

    ' : '

    ' . charitable_format_money( $activity->amount, 2, true ) . $campaign_title . '

    '; break; default: $admin_campaign_url = ! empty( $activity->campaign_id ) ? charitable_get_admin_campaign_edit_url( $activity->campaign_id ) : '#'; $admin_donation_url = ! empty( $activity->item_id ) ? charitable_get_admin_donation_edit_url( $activity->item_id ) : false; $campaign_title = ! empty( $activity->campaign_title ) ? ' to ' . esc_html( $activity->campaign_title ) . ' ' : ''; $secondary_info = $admin_donation_url ? '

    ' . charitable_format_money( $activity->amount, 2, true ) . '' . $campaign_title . '

    ' : '

    ' . charitable_format_money( $activity->amount, 2, true ) . $campaign_title . '

    '; break; } return $secondary_info; } /** * Generate HTML for the "secondary" info for a campaign activity. * * @since 1.8.1 * * @param object $activity The activity data. * * @return string */ public function get_campaign_activity_secondary_info( $activity ) { $secondary_info = ''; switch ( $activity->primary_action ) { case 'update': $secondary_info = '

    ' . esc_html( $activity->campaign_title ) . '

    '; break; default: $secondary_info = '

    ' . esc_html( $activity->campaign_title ) . '

    '; break; } return $secondary_info; } /** * Generates HTML for the payment methods list. * * @since 1.8.1 * * @param array $payment_breakdown The payment breakdown data. * @param bool $include_icons Whether to include icons. * * @return string */ public function generate_payment_methods_list( $payment_breakdown = array(), $include_icons = true ) { ob_start(); if ( empty( $payment_breakdown ) ) { ?>
  • -
  • $data ) : $percentage = intval( $data['donors'] ) / $payment_breakdown['donations_total'] * 100; // round to nearest .1 number. $percentage = round( $percentage, 1 ); $title = ! empty( $data['title'] ) ? 'title="' . esc_html( $data['title'] ) . '"' : false; ?>
  • >
    >
    %

  • $data ) : $net = 0; $refund_count = 0; $css_class = false; $prefix = false; $row_id = strtotime( $day ); $net = $data['amount'] - $refunds_data['refunds_by_day'][ $day ]['amount']; $prefix = intval( $net ) < 0 ? '-' : '+'; $css_class = intval( $net ) < 0 ? 'negative' : 'positive'; $css_class = intval( $net ) === 0 ? 'zero' : $css_class; $refund_count = intval( $refunds_data['refunds_by_day'][ $day ]['donors'] ); $refund_amount = $refunds_data['refunds_by_day'][ $day ]['amount']; ?> esc_html__( 'Donors', 'charitable' ), 'columns' => array( 'avatar' => '', 'name-email' => esc_html__( 'Name', 'charitable' ), 'total-donations' => esc_html__( 'Total Donations', 'charitable' ), 'number-donations' => esc_html__( 'Number of Donations', 'charitable' ), 'average' => esc_html__( 'Average', 'charitable' ), 'number-campaigns' => esc_html__( 'Number of Campaigns', 'charitable' ), 'last-donation' => esc_html__( 'Last Donation', 'charitable' ), 'actions' => esc_html__( 'Actions', 'charitable' ), ), ); break; case 'donors-recurring': $table_meta = array( 'title' => esc_html__( 'Donors', 'charitable' ), 'columns' => array( 'avatar' => '', 'name-email' => esc_html__( 'Name', 'charitable' ), 'total-donations' => esc_html__( 'Total Donations', 'charitable' ), 'number-donations' => esc_html__( 'Number of Donations', 'charitable' ), 'average' => esc_html__( 'Average', 'charitable' ), 'number-campaigns' => esc_html__( 'Number of Campaigns', 'charitable' ), 'last-donation' => esc_html__( 'Last Donation', 'charitable' ), 'actions' => esc_html__( 'Actions', 'charitable' ), ), ); break; case 'donors-first-time': $table_meta = array( 'title' => esc_html__( 'Donors', 'charitable' ), 'columns' => array( 'avatar' => '', 'name-email' => esc_html__( 'Name', 'charitable' ), 'total-donations' => esc_html__( 'Donation', 'charitable' ), 'last-donation' => esc_html__( 'Campaign', 'charitable' ), 'date' => esc_html__( 'Date', 'charitable' ), 'actions' => esc_html__( 'Actions', 'charitable' ), ), ); break; default: // code... break; } return $table_meta; } /** * Generates HTML for the donor breakdown table on the overview page. * * @since 1.8.1 * @since 1.8.3 Added $limit_per_page parameter and generate_pagination_html(). * * @param string $report_type The report type. * @param array $donor_breakdown The donor breakdown data. * @param int $current_page The current page. * @param int $limit_per_page The limit per page. * * @return string */ public function generate_donor_breakdown_table_html( $report_type = 'donors-top', $donor_breakdown = array(), $current_page = 1, $limit_per_page = 10 ) { $table_meta = $this->get_donor_report_table_meta( $report_type ); ob_start(); ?>


    $column_title ) : if ( $column_slug === 'avatar' ) { echo ''; } else { echo ''; } endforeach; ?> generate_donor_breakdown_rows( $donor_breakdown, $report_type ); // phpcs:ignore ?> $column_title ) : if ( $column_slug === 'avatar' ) { echo ''; } else { echo ''; } endforeach; ?>
    ' . esc_html( $column_title ) . '
    ' . esc_html( $column_title ) . '

    generate_pagination_html( $limit_per_page, $current_page, $report_type ); // phpcs:ignore ?>
    $data ) : $donor_id = isset( $data->donor_id ) ? intval( $data->donor_id ) : 0; $donor_email = isset( $data->email ) ? esc_html( $data->email ) : ''; $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . '/images/misc/placeholder-avatar-small.jpg'; $donor_name = esc_html( $data->first_name ) . ' ' . esc_html( $data->last_name ); $donor_number_donations = isset( $data->total_count_donations ) ? intval( $data->total_count_donations ) : 0; $donor_number_campaigns = isset( $data->total_count_campaigns ) ? intval( $data->total_count_campaigns ) : 0; $donor_average = $donor_number_donations > 0 ? $data->total_amount / $donor_number_donations : 0; $donation_id = isset( $data->donation_id ) ? intval( $data->donation_id ) : 0; $is_sample_data = isset( $data->sample ) ? true : false; // date format: M j, Y plus time of day in am/pm. $last_donation_date = $data->last_donation_date ? gmdate( 'M j, Y g:i a', strtotime( $data->last_donation_date ) ) : 'N/A'; $last_donation_campaign_name = $data->last_campaign_title ? esc_html( $data->last_campaign_title ) : 'N/A'; $last_donation_campaign_id = $data->last_campaign_id ? intval( $data->last_campaign_id ) : 0; $action_link = '#'; $action_label = false; // Determine the (view) link. if ( 'donors-first-time' === $report_type && $donation_id > 0 ) { $action_link = ( $donor_id !== 0 ) ? admin_url( 'post.php?post=' . $donation_id . '&action=edit' ) : '#'; $action_label = esc_html__( 'View Donation', 'charitable' ); } elseif ( $last_donation_campaign_id > 0 ) { // Does this post exist? if ( false === get_post_status( $last_donation_campaign_id ) ) { $action_link = '#'; } else { // Determine if this is a "legacy" campaign. $is_legacy_campaign = charitable_is_campaign_legacy( $last_donation_campaign_id ); $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; $action_label = 'donors-top' === $report_type ? esc_html__( 'View Last Donated Campaign', 'charitable' ) : esc_html__( 'View Campaign', 'charitable' ); } } if ( 'donors-first-time' === $report_type ) { if ( $last_donation_campaign_name && $last_donation_campaign_id ) { $last_donation_html = '' . $last_donation_campaign_name . ''; } elseif ( $is_sample_data ) { $last_donation_html = $last_donation_campaign_name; } } elseif ( $last_donation_date && $last_donation_campaign_name && $last_donation_campaign_id ) { $last_donation_html = charitable_format_money( esc_html( $data->amount ), 2, true ) . '
    ' . $last_donation_campaign_name . '
    ' . $last_donation_date; } elseif ( $is_sample_data ) { $last_donation_html = charitable_format_money( esc_html( $data->amount ), 2, true ) . '
    ' . $last_donation_campaign_name . '
    ' . $last_donation_date; } if ( $report_type === 'donors-first-time' ) { ?> <?php echo esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>
    total_amount ), 2, true ); // phpcs:ignore ?> '; if ( $action_label !== false ) { $display .= '' . $action_label . ''; } $display .= ''; echo $display; // phpcs:ignore ?> <?php echo esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>
    total_amount ), 2, true ); // phpcs:ignore ?> '; if ( $action_label !== false ) { $display .= '' . $action_label . ''; } $display .= ''; echo $display; // phpcs:ignore ?> get_localized_strings(); ?> donations; } /** * Get the donations stored in the class, and return sorted/organized by payment method. * * @since 1.8.1 * * @return array */ public function get_donations_by_payment() { if ( empty( $this->donations ) ) { return; } // setup array so that it holds all the days in the date range. $donations_by_payment = apply_filters( 'charitable_reports_default_payment_types', array( 'stripe' => array( 'label' => esc_html__( 'Stripe', 'charitable' ), 'amount' => 0, 'donors' => 0, 'title' => esc_html__( 'Donations made through the Stripe gateway.', 'charitable' ), ), 'paypal' => array( 'label' => esc_html__( 'PayPal', 'charitable' ), 'amount' => 0, 'donors' => 0, 'title' => esc_html__( 'Donations made through the PayPal gateway.', 'charitable' ), ), 'manual' => array( 'label' => esc_html__( 'Manual', 'charitable' ), 'amount' => 0, 'donors' => 0, 'title' => esc_html__( 'Donations manually entered into the WordPress admin.', 'charitable' ), ), 'offline' => array( 'label' => esc_html__( 'Offline', 'charitable' ), 'amount' => 0, 'donors' => 0, 'title' => esc_html__( 'Donations made outside of the website.', 'charitable' ), ), ) ); // Go through the array and group the amounts and number of donors by day. foreach ( $this->donations as $key => $donation ) { $payment = ( ! empty( $donation->payment_gateway ) ? strtolower( esc_attr( $donation->payment_gateway ) ) : $payment = 'unknown' ); // Get the proper label from the gateway object if available. $label = ''; $gateways = charitable_get_helper( 'gateways' ); if ( $gateways && $gateways->is_valid_gateway( $payment ) ) { $gateway_object = $gateways->get_gateway_object( $payment ); if ( $gateway_object ) { $label = $gateway_object->get_label(); } } // Fallback to manual label generation if gateway object is not available. if ( empty( $label ) ) { // cap the P in Paypal manually. $label = ( 'paypal' === strtolower( $donation->payment_gateway ) ) ? 'PayPal' : ucwords( str_replace( '_', ' ', $donation->payment_gateway ) ); } if ( isset( $donations_by_payment[ $payment ] ) ) { // Update the label if it wasn't set in the default array. if ( ! empty( $label ) ) { $donations_by_payment[ $payment ]['label'] = esc_html( $label ); } $donations_by_payment[ $payment ]['amount'] += $donation->total_amount; $donations_by_payment[ $payment ]['donors'] += 1; } else { $donations_by_payment[ $payment ]['label'] = esc_html( $label ); $donations_by_payment[ $payment ]['amount'] = $donation->total_amount; $donations_by_payment[ $payment ]['donors'] = 1; $donations_by_payment[ $payment ]['title'] = $donation->title; } } $this->payment_percentages = array(); $this->payment_labels = array(); $this->payment_keys = array(); foreach ( $donations_by_payment as $key => $donation ) { $percentage = intval( $donation['donors'] ) / count( $this->donations ) * 100; // round to nearest .1 number. $percentage = round( $percentage, 1 ); $this->payment_percentages[] = $percentage; $this->payment_labels[] = $donation['label']; // . '% (' . charitable_format_money( $donation['amount'] ) . ')'; $this->payment_keys[] = $key; // Store the payment gateway key for color mapping. } if ( empty( $this->payment_labels ) && empty( $this->payment_percentages ) ) { // no donations were likely found so let's add the "not found" data. $this->payment_percentages[] = 100; $this->payment_labels[] = __( 'No donations found.', 'charitable' ); $this->payment_keys[] = 'none'; } else { $this->payment_percentages[] = 0; $this->payment_labels[] = ''; $this->payment_keys[] = ''; } $donations_by_payment = apply_filters( 'charitable_reports_donations_by_payment_data', $donations_by_payment ); return array( 'donations' => $donations_by_payment, 'donations_total' => count( $this->donations ), ); } /** * Get unique donor count from donations. * * @since 1.8.1 * * @return int */ public function get_donors_count() { if ( empty( $this->donations ) ) { return 0; } // Go through multi-dimenssional array and get a count of all unique values of donor_id. $donor_ids = array(); $donation_total = 0; foreach ( $this->donations as $donation ) { // Add the donor ID to the array. $donor_ids[] = $donation->donor_id; // Add the donation amount to the total. $donation_total += $donation->total_amount; } return ( count( array_unique( $donor_ids ) ) ); } /** * Get donation amount total from donations. * * @since 1.8.1 * * @return float */ public function get_donations_total() { if ( empty( $this->donations ) ) { return 0; } // Go through multi-dimenssional array and get a count of all unique values of donor_id. $donation_total = 0; foreach ( $this->donations as $donation ) { // Add the donation amount to the total. $donation_total += $donation->total_amount; } return ( (float) $donation_total ); } /** * Get a breakdown of donations per day based on the start and end dates. * * @since 1.8.1 * * @param bool $format_money Whether to format the money (say for downloadable reports ). * * @return array */ public function get_donations_by_day( $format_money = false ) { if ( empty( $this->donations ) ) { return; } $campaign_donations_table = charitable_get_table( 'campaign_donations' ); // setup array so that it holds all the days in the date range. $donations_by_day = array(); $days = $this->get_days_between_dates( $this->start_date, $this->end_date ); $refunded_statuses = array( 'charitable-refunded' ); foreach ( $days as $day_unformatted => $day_label ) { $donation_today_completed = $campaign_donations_table->get_donations_summary_by_period( gmdate( 'Y-m-d', strtotime( $day_unformatted ) ), array( 'charitable-completed', 'charitable-refunded' ) ); $donation_today_refunded = $campaign_donations_table->get_donations_summary_by_period( gmdate( 'Y-m-d', strtotime( $day_unformatted ) ), $refunded_statuses ); $donation_day = gmdate( 'Y-m-d', strtotime( $day_unformatted ) ); $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day_unformatted ) ); $donations_by_day[ $donation_day ]['amount'] = $donation_today_completed->amount; $donations_by_day[ $donation_day ]['donors'] = 0; $donations_by_day[ $donation_day ]['refunds'] = $donation_today_refunded->amount; $donations_by_day[ $donation_day ]['net'] = 0; } // Go through the array and group the amounts and number of donors by day. foreach ( $this->donations as $donation ) { $donation_day = gmdate( 'Y-m-d', strtotime( $donation->post_date ) ); if ( ! empty( $donation->email ) ) { if ( isset( $unique_donors[ $donation_day ][ $donation->email ] ) ) { $unique_donors[ $donation_day ][ $donation->email ] = $unique_donors[ $donation_day ][ $donation->email ] + 1; } else { $unique_donors[ $donation_day ][ $donation->email ] = 1; } } if ( isset( $donations_by_day[ $donation_day ] ) ) { $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $donation->post_date ) ); // $donations_by_day[ $donation_day ]['refunds'] += in_array( $donation->donation_status, $refunded_statuses, true ) ? $donation->total_amount : 0; } else { $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $donation->post_date ) ); // $donations_by_day[ $donation_day ]['refunds'] = in_array( $donation->donation_status, $refunded_statuses, true ) ? $donation->total_amount : 0; } } // 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. foreach ( $donations_by_day as $day => $data ) { $donations_by_day[ $day ]['net'] = $this->get_net( $data['amount'], $data['refunds'] ); $donations_by_day[ $day ]['donors'] = ! empty( $unique_donors[ $day ] ) ? count( $unique_donors[ $day ] ) : 0; if ( $format_money ) : $donations_by_day[ $day ]['amount'] = $this->charitable_reports_format_money( $data['amount'] ); $donations_by_day[ $day ]['refunds'] = $this->charitable_reports_format_money( $data['refunds'] ); $donations_by_day[ $day ]['net'] = $this->charitable_reports_format_money( $donations_by_day[ $day ]['net'] ); endif; } // reverse array. $donations_by_day = array_reverse( $donations_by_day ); return $donations_by_day; } /** * Get the total amount of refunds from the stored activities. * * @since 1.8.1 * * @param int $campaign_id The campaign ID. * @param int $limit The limit of activities to get. * * @return float */ public function get_activity_refund_total( $campaign_id = false, $limit = false ) { if ( $this->is_activities_empty() ) { $this->activities = $this->get_activity( $campaign_id, $limit ); } if ( $this->is_activities_empty() ) { return 0; } $refunded_total = 0; foreach ( $this->activities as $activity ) { if ( 'refunded' === $activity->status ) { $refunded_total += $activity->amount; } } return $refunded_total; } /** * Get the total count of refunds from the stored activities. * * @since 1.8.1 * * @param int $campaign_id The campaign ID. * @param int $limit The limit of activities to get. * * @return int */ public function get_activity_refund_count( $campaign_id = false, $limit = false ) { if ( $this->is_activities_empty() ) { $this->activities = $this->get_activity( $campaign_id, $limit ); } if ( $this->is_activities_empty() ) { return 0; } $refunded_count = 0; foreach ( $this->activities as $activity ) { if ( 'refunded' === $activity->status ) { ++$refunded_count; } } return $refunded_count; } /** * Get activity for the report. * * @since 1.8.1 * * @param array $args The array of args. * * @return array */ public function get_activity( $args = array() ) { $defaults = array( 'activity_filter_types' => array(), 'activity_action_types' => array(), 'start_date' => false, 'end_date' => false, 'status' => false, 'campaign_id' => false, 'category_id' => false, 'limit' => false, 'offset' => false, 'ppage' => false, ); $args = wp_parse_args( $args, $defaults ); // Extract individual variables from args array. $activity_filter_types = $args['activity_filter_types'] ?? array(); $activity_action_types = $args['activity_action_types'] ?? array(); $start_date = $args['start_date'] ?? false; $end_date = $args['end_date'] ?? false; $status = $args['status'] ?? false; $campaign_id = $args['campaign_id'] ?? false; $category_id = $args['category_id'] ?? false; $limit = $args['limit'] ?? false; $offset = $args['offset'] ?? false; $ppage = $args['ppage'] ?? false; if ( ! class_exists( 'Charitable_Admin_Activities' ) || ! class_exists( 'Charitable_Donation_Activities_DB' ) || ! class_exists( 'Charitable_Campaign_Activities_DB' ) ) { return; } $charitable_donation_activities_db = Charitable_Donation_Activities_DB::get_instance(); // check to see if db tables exist. if ( ! $charitable_donation_activities_db->table_exists() ) { return; } $charitable_campaign_activities_db = Charitable_Campaign_Activities_DB::get_instance(); // check to see if db tables exist. if ( ! $charitable_campaign_activities_db->table_exists() ) { return; } // If not passed, grab from the local vars in the class. $campaign_id = $campaign_id ? $campaign_id : $this->campaign_id; $category_id = absint( $category_id ) > 0 ? $category_id : $this->category_id; $admin_activities = new Charitable_Admin_Activities(); // Get the "raw" activities data. $activities = $admin_activities->get_activity( array( 'activity_filter_types' => $activity_filter_types, 'activity_action_types' => $activity_action_types, 'campaign_id' => $campaign_id, 'category_id' => $category_id, 'start_date' => $this->start_date, 'end_date' => $this->end_date, 'limit' => $limit, 'offset' => $offset, ) ); // Get the more "formatted" (with additional data) reports data. $report_activities = $admin_activities->get_activity_report_data( $activities ); return $report_activities; } /** * Generate Pagination HTML * * @since 1.8.1 * * @param int $total_pages The total number of pages. * @param int $current_page The current page. * @param int $total_records Total number of records. * @param int $offset The offset. * @param int $limit The limit. * @param array $arr_params The params for the pagination links, which might depend on report type. * @param int $dot_threshold The dot threshold. * * @return void */ public function show_pagination( $total_pages = 1, $current_page = 1, $total_records = false, $offset = false, $limit = 10, $arr_params = array(), $dot_threshold = false ) { // Only display if pro. if ( ! charitable_is_pro() ) { return; } if ( false === $dot_threshold ) { $dot_threshold = $this->pagination_dot_threshold; } $total_pages = intval( $total_pages ); echo ''; return; } echo ''; } /** * Get link for the charitable pagination * * @since 1.8.1.3 * * @param string $total_pages Current page number. * @param string $current_page Current page. * @param string $total_records Total number of records. * @param string $offset The offset. * @param string $ppage The ppage. * @param string $limit The limit. * @param array $arr_params Params to build the attributes. * * @return string */ public function get_pagination_data_attributes( $total_pages = '', $current_page = '', $total_records = 0, $offset = 0, $ppage = 1, $limit = 10, $arr_params = array() ) { $data_attributes = array( 'total-pages' => $total_pages, 'current-page' => $current_page, 'total-records' => $total_records, // 'offset' => $offset, 'ppage' => $ppage, // a page in the results. 'limit' => $limit, // the limit of items per page. ); if ( ! empty( $arr_params ) ) { foreach ( $arr_params as $key => $value ) { $data_attributes[ sanitize_title( $key ) ] = $value; } } // create a string of data attributes for an HTML element based on $data_attributes. $data_attributes_string = ''; foreach ( $data_attributes as $key => $value ) { $data_attributes_string .= ' data-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"'; } return $data_attributes_string; } /** * Get link for the charitable pagination * * @since 1.8.1 * * @param string $page_number Current page number. * @param array $arr_params Params to build the URL. * * @return array */ public function get_pagination_url( $page_number, $arr_params = array() ) { $arr_params['ppage'] = $page_number; return add_query_arg( $arr_params, admin_url( 'admin.php' ) ); } /** * Generate the HTML for the pagination. * * @since 1.8.1 * * @param int $limit The limit of items per page. * @param int $current_page The (current) page number. * @param string $report_type The report type. * * @return float */ public function generate_pagination_html( $limit = 10, $current_page = 1, $report_type = 'donors-top' ) { $donors = (array) $this->donors; $limit = charitable_reports_is_ajax() && isset( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? intval( $_POST['limit'] ) : $limit; // phpcs:ignore $limit = false !== $limit && is_numeric( $limit ) ? $limit : charitable_reports_get_pagination_per_page(); $current_page = charitable_reports_is_ajax() && isset( $_POST['ppage'] ) && is_numeric( $_POST['ppage'] ) ? intval( $_POST['ppage'] ) : $current_page; // phpcs:ignore $current_page = is_numeric( $current_page ) ? $current_page : 1; $pagination = array( 'length' => isset( $_GET['limit'] ) ? (int) $_GET['limit'] : $limit, // phpcs:ignore 'total' => 0 !== $this->donors_total ? $this->donors_total : count( $donors ), 'currentPage' => isset( $_GET['ppage'] ) ? (int) $_GET['ppage'] : $current_page, // phpcs:ignore ); $pagination['total_pages'] = ceil( $pagination['total'] / $pagination['length'] ); $pagination['offset'] = ( $pagination['currentPage'] * $pagination['length'] ) - $pagination['length']; // The params for the pagination links, which might depend on report type. $arr_params = array( 'page' => 'charitable-reports', 'tab' => 'donors', 'report-type' => isset( $_GET['report_type'] ) ? sanitize_text_field( wp_unslash( $_GET['report_type'] ) ) : $report_type, // phpcs:ignore ); ob_start(); $this->show_pagination( $pagination['total_pages'], $pagination['currentPage'], $pagination['total'], $pagination['offset'], $pagination['length'], $arr_params ); return ob_get_clean(); } /** * Get the activity icon url from a status string. * * @since 1.8.1 * * @param string $icon The icon name. * * @return string */ public function get_activity_icon_url( $icon = false ) { if ( false === $icon || '' === $icon ) { return false; } return charitable()->get_path( 'directory', false ) . 'assets/images/icons/activities/' . $icon . '.png'; } /** * Get the top donors for the dedicated donor report. * * @since 1.8.1 * * @param int $limit The limit of donors to get. * @param int $offset The offset of donors to get. * * @return string */ public function get_top_donors( $limit = false, $offset = false ) { // phpcs:ignore if ( empty( $this->donors ) ) { return; } return $this->donors; } /** * Get the top donors for the dedicated donor report. * * @since 1.8.1 * * @param int $limit The limit of donors to get. * * @return string */ public function get_recurring_donors( $limit = false ) { // phpcs:ignore if ( empty( $this->donors ) ) { return; } return $this->donors; } /** * Get the first time donors for the dedicated donor report. * * @since 1.8.1 * * @param int $limit The limit of donors to get. * * @return string */ public function get_first_time_donors( $limit = false ) { // phpcs:ignore if ( empty( $this->donors ) ) { return; } return $this->donors; } /** * Get the donors for the dedicated donor report. * * @since 1.8.1 * * @param string $report_type The report type. * @param array $args The array of args. * * @return array|bool */ public function get_donor_report_by_type( $report_type = false, $args = array() ) { $defaults = array( 'limit' => false, 'offset' => false, 'ppage' => false, ); $args = wp_parse_args( $args, $defaults ); // Extract individual variables from args array. $limit = $args['limit'] ?? false; $offset = $args['offset'] ?? false; $ppage = $args['ppage'] ?? false; if ( ! $offset && $ppage && $limit ) { $offset = ( $ppage - 1 ) * $limit; } switch ( $report_type ) { case 'donors-top': return ( $this->get_top_donors( $limit, $offset ) ); break; // phpcs:ignore case 'donors-recurring': return ( $this->get_recurring_donors( $limit, $offset ) ); break; // phpcs:ignore case 'donors-first-time': return ( $this->get_first_time_donors( $limit, $offset ) ); break; // phpcs:ignore default: return false; break; // phpcs:ignore } } /** * Return the HTML of the filter select dropdown. * * @since 1.8.1 * * @param string $selected_value The selected value. * * @return string */ public function get_donor_report_type_dropdown( $selected_value = false ) { $options_values = apply_filters( 'charitable_dashboard_donor_report_type_filter_values', array( 'donors-top' => __( 'Top Donors', 'charitable' ), 'donors-first-time' => __( 'First Time Donors', 'charitable' ), 'donors-recurring' => __( 'Recurring Donors', 'charitable' ), ) ); ob_start(); ?> donations ) ) { return; } $donations_by_donors = (array) $this->donations; $unique_donors = array(); // sort multi-dimensional array by 'amount'. usort( $donations_by_donors, function ( $a, $b ) { return $b->total_amount - $a->total_amount; } ); foreach ( $donations_by_donors as $index => $donation ) { if ( empty( $donation->donor_id ) || 0 === $donation->donor_id ) { continue; } $donor = charitable_get_table( 'donors' )->get_personal_data_by_donor_id( $donation->donor_id ); if ( empty( $donor ) ) { continue; } $donor = $donor[0]; // combine first and last name from $donor into $donor_name, if either exist. $donor_name = ( ! empty( $donor->first_name ) || ! empty( $donor->last_name ) ? $donor->first_name . ' ' . $donor->last_name : 'Unknown' ); $donor_email = ( ! empty( $donor->email ) ) ? $donor->email : ''; $donations_by_donors[ $index ]->donor_name = trim( $donor_name ); $donations_by_donors[ $index ]->donor_email = $donor_email; $donations_by_donors[ $index ]->donor_avatar_url = isset( $donation->user_id ) && $donation->user_id !== 0 ? get_avatar_url( $donation->user_id, 32 ) : 0; $donations_by_donors[ $index ]->donation_type = charitable_get_donation( $donation->donation_id )->get( 'donation_type' ); $donations_by_donors[ $index ]->donation_parent_id = $donation->donation_parent_id; if ( ! isset( $unique_donors[ $donation->donor_id ] ) ) { $unique_donors[ $donation->donor_id ]['amount'] = $donation->total_amount; $unique_donors[ $donation->donor_id ]['count'] = 1; } else { $unique_donors[ $donation->donor_id ]['amount'] += $donation->total_amount; $unique_donors[ $donation->donor_id ]['count'] = $unique_donors[ $donation->donor_id ]['count'] + 1; } } // go back and add the total donation amount to the $donations_by_donors array. foreach ( $donations_by_donors as $index => $donation ) { if ( ! empty( $unique_donors[ $donation->donor_id ] ) ) { $donations_by_donors[ $index ]->total_donation_amount = $unique_donors[ $donation->donor_id ]['amount']; $donations_by_donors[ $index ]->total_donation_count = $unique_donors[ $donation->donor_id ]['count']; } } // sort multi-dimensional array by 'total_donation_amount'. // @since 1.8.8.6 - Fixed undefined property warning by checking if property exists before accessing. usort( $donations_by_donors, function ( $a, $b ) { // @since 1.8.8.6 - Check if total_donation_amount exists, default to 0 if not set. $a_amount = isset( $a->total_donation_amount ) ? $a->total_donation_amount : 0; $b_amount = isset( $b->total_donation_amount ) ? $b->total_donation_amount : 0; return $b_amount - $a_amount; } ); return $donations_by_donors; } /** * Get the name of the cache value (or key) for the report we use for the database. * * @since 1.8.1 * * @param string $report_type The report type. * @param string $start_date The start date. * @param string $end_date The end date. * @param string $post_status The post status. * @param string $campaign_id The campaign ID. * @param string $category_id The category ID. * * @return string */ public function get_cache_key( $report_type = 'overview', $start_date = false, $end_date = false, $post_status = false, $campaign_id = false, $category_id = false ) { $cache_key = sanitize_title( $start_date . $end_date . str_replace( 'charitable', '', $post_status ) . $campaign_id . $category_id ); // remove all dashes from the cache key. $cache_key = 'wpch-report-' . $report_type . '-' . str_replace( '-', '', $cache_key ); return $cache_key; } /** * Get the cached report. * * @since 1.8.1 * * @param string $report_type The report type. * @param string $start_date The start date. * @param string $end_date The end date. * @param string $post_status The post status. * @param string $campaign_id The campaign ID. * @param string $category_id The category ID. * * @return array|bool */ public function get_cached_report( $report_type = 'overview', $start_date = false, $end_date = false, $post_status = false, $campaign_id = false, $category_id = false ) { if ( ! $this->maybe_cache_report( true ) ) { return false; } $cache_key = $this->get_cache_key( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id ); $data = get_transient( $cache_key ); if ( false === $data ) { return false; } return $data; } /** * Set the cached report. * * @since 1.8.1 * * @param string $report_type The report type. * @param string $start_date The start date. * @param string $end_date The end date. * @param string $post_status The post status. * @param string $campaign_id The campaign ID. * @param string $category_id The category ID. * @param array $data The data to cache. * * @return bool */ 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() ) { if ( ! $this->maybe_cache_report( true ) ) { return false; } $cache_key = $this->get_cache_key( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id ); $was_set = set_transient( $cache_key, $data, MINUTE_IN_SECONDS ); return $was_set; } /** * Check if the report should be cached. * * @since 1.8.1 * * @param bool $force_cache Whether to force the cache. * * @return bool */ public function maybe_cache_report( $force_cache = false ) { // Check the globals to see if caching is enabled specifically for reports. if ( $force_cache ) { return true; } if ( charitable_is_debug() ) { return false; } if ( defined( 'CHARITABLE_REPORTS_NO_CACHE' ) && CHARITABLE_REPORTS_NO_CACHE ) { // phpcs:ignore return false; } return true; } /** * This accepts a report type and date and attemps to create a report that has multiple data points * * @param string $report_type The type of report to create. * @param array $args The array of args. * * @since 1.8.1 * * @return array */ public function get_data( $report_type = 'overview', $args = array() ) { global $wpdb; $defaults = array( 'report_type' => 'overview', 'start_date' => false, 'end_date' => false, 'post_status' => 'charitable-completed', 'campaign_id' => false, 'category_id' => false, ); $args = wp_parse_args( $args, $defaults ); // Extract individual variables from args array. $report_type = $args['report_type'] ?? 'overview'; $start_date = $args['start_date'] ?? false; $end_date = $args['end_date'] ?? false; $post_status = $args['post_status'] ?? 'charitable-completed'; $campaign_id = $args['campaign_id'] ?? false; $category_id = $args['category_id'] ?? false; // Check the post_status and set it to the default if it's not a valid status. if ( ! in_array( $post_status, array( 'charitable-completed', 'charitable-pending', 'charitable-failed', 'charitable-cancelled', 'charitable-refunded' ), true ) ) { $post_status = 'charitable-completed'; } $this->donations = ! charitable_is_pro() && $report_type !== 'overview' ? $this->get_data_sample( $report_type ) : false; // Determine if this particular report has been cached recently. $this->donations = false; // $this->get_cached_report( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id ); if ( false === $this->donations ) { // No cache? Let's run the data. $where_sql = array(); $where_sql[] = 'WHERE 1=1'; $where_sql[] = ( intval( $campaign_id ) > 0 ) ? 'cd.campaign_id = ' . intval( $campaign_id ) : false; $where_sql[] = 'p.post_type = "%s"'; $where_sql[] = 'p.post_status = "' . $post_status . '"'; $where_sql[] = ( $start_date ) ? "p.post_date >= '" . $start_date . " 00:00:00'" : false; $where_sql[] = ( $end_date ) ? "p.post_date <= '" . $end_date . " 23:59:59'" : false; $where_sql[] = ( intval( $category_id ) > 0 ) ? 'tr.term_taxonomy_id = ' . intval( $category_id ) : false; $where_sql[] = 'pm1.meta_key = "donation_gateway"'; // remove all empty values from the array. $where_sql = array_filter( $where_sql ); $where_args = implode( ' AND ', $where_sql ); $left_join = array(); $left_join[] = $wpdb->prefix . 'charitable_campaign_donations cd ON p.ID = cd.donation_id'; $left_join[] = $wpdb->prefix . 'charitable_donors cdonors ON cd.donor_id = cdonors.donor_id'; $left_join[] = $wpdb->prefix . 'postmeta pm1 ON p.ID = pm1.post_id'; $left_join[] = ( intval( $category_id ) > 0 ) ? $wpdb->prefix . 'term_relationships tr ON tr.object_id = campaign_id' : false; // remove all empty values from the array. $left_join = array_filter( $left_join ); $left_join_args = 'LEFT JOIN ' . implode( ' LEFT JOIN ', $left_join ); $sql = "SELECT SUM(cd.amount) AS total_amount, cd.donation_id AS donation_id, cd.campaign_id AS campaign_id, cd.donor_id AS donor_id, cd.amount AS amount, pm1.meta_value AS payment_gateway, p.post_date AS post_date, p.post_date_gmt AS post_date_gmt, p.post_status AS donation_status, p.post_parent AS donation_parent_id, cdonors.first_name AS first_name, cdonors.last_name AS last_name, cdonors.email AS email FROM $wpdb->posts p " . $left_join_args . ' ' . $where_args . ' GROUP BY p.ID'; // get all donations (posts with a post type of donation) between a start date and an end date. $this->donations = $wpdb->get_results( // phpcs:ignore $wpdb->prepare( $sql, // phpcs:ignore 'donation' ) ); // Save this to the cache. // $this->set_cached_report( $report_type, $start_date, $end_date, $post_status, $campaign_id, $category_id, $this->donations );. } $this->init_axis_with_donations( $this->donations, $start_date, $end_date ); return $this->donations; } /** * This accepts a report type and date and attemps to create a report specific to a donor type report. * * @since 1.8.1 * * @param string $report_type The type of report to create. * @param string $start_date The start date. * @param string $end_date The end date. * @param string $campaign_id The campaign ID. * @param string $category_id The category ID. * * @return array */ public function get_donors_data( $report_type = 'donors-top', $start_date = false, $end_date = false, $campaign_id = false, $category_id = false ) { // phpcs:ignore global $wpdb; // Determine if this particular report gets sample data, or if not if it's has been cached recently. $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 ); if ( false === $this->donors ) { // No cache? Let's run the data. $having_sql = ''; $order_by = ''; switch ( $report_type ) { case 'donors-recurring': $having_sql = 'HAVING total_count_donations > 1'; $order_by = 'ORDER BY %s DESC'; $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_recurring_sql_limit', 100 ); $sql = "SELECT SUM( ccd.amount ) AS total_amount, AVG( ccd.amount ) AS average, COUNT( ccd.donation_id ) AS total_count_donations, COUNT( DISTINCT( ccd.campaign_id ) ) AS total_count_campaigns, last_donation.post_date AS last_donation_date, last_campaign.post_title AS last_campaign_title, last_campaign.ID AS last_campaign_id, 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 FROM {$wpdb->prefix}charitable_donors cd JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id 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 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 GROUP BY cd.donor_id {$having_sql} {$order_by} {$limit} "; $this->donors = $wpdb->get_results( // phpcs:ignore $wpdb->prepare( $sql, // phpcs:ignore 'total_amount' ) ); break; case 'donors-first-time': $having_sql = 'HAVING total_count_donations = 1'; $order_by = 'ORDER BY %s DESC'; // not currently being used. $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_first_time_sql_limit', 100 ); $sql = "SELECT ccd.amount AS total_amount, COUNT( ccd.donation_id ) AS total_count_donations, last_donation.post_date AS last_donation_date, last_campaign.post_title AS last_campaign_title, last_campaign.ID AS last_campaign_id, 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 FROM {$wpdb->prefix}charitable_donors cd JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id 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 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 GROUP BY cd.donor_id {$having_sql} ORDER BY last_donation_date DESC {$limit} "; $this->donors = $wpdb->get_results( // phpcs:ignore $wpdb->prepare( $sql // phpcs:ignore ) ); break; default: $order_by = 'ORDER BY %s DESC'; $limit = 'LIMIT ' . apply_filters( 'charitable_dashboard_donor_report_default_sql_limit', 100, $report_type ); $sql = "SELECT SUM( ccd.amount ) AS total_amount, AVG( ccd.amount ) AS average, COUNT( ccd.donation_id ) AS total_count_donations, COUNT( DISTINCT( ccd.campaign_id ) ) AS total_count_campaigns, last_donation.post_date AS last_donation_date, last_campaign.post_title AS last_campaign_title, last_campaign.ID AS last_campaign_id, 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 FROM {$wpdb->prefix}charitable_donors cd JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id 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 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 GROUP BY cd.donor_id {$having_sql} {$order_by} {$limit} "; $this->donors = $wpdb->get_results( // phpcs:ignore $wpdb->prepare( $sql, // phpcs:ignore 'total_amount' ) ); break; } // Save this to the cache. // $this->set_cached_report( $report_type, $start_date, $end_date, false, $campaign_id, $category_id, $this->donors );. return $this->donors; } } /** * This accepts a report type and date and attemps to create a report that has multiple data points * * @since 1.8.1 * @since 1.8.3 Updated w/ limit and offset sql, etc. * * @param string $report_type The type of report to create. * @param array $args The array of args. * * @return array */ public function get_donors_data_array( $report_type = 'donors-top', $args = array() ) { global $wpdb; $defaults = array( 'report_type' => 'donors-top', 'start_date' => false, 'end_date' => false, 'campaign_id' => false, 'category_id' => false, 'limit' => false, 'offset' => false, ); $args = wp_parse_args( $args, $defaults ); // Extract individual variables from args array. $report_type = $args['report_type'] ?? 'donors-top'; $start_date = $args['start_date'] ?? false; $end_date = $args['end_date'] ?? false; $campaign_id = $args['campaign_id'] ?? false; $category_id = $args['category_id'] ?? false; $limit = $args['limit'] ?? false; $offset = $args['offset'] ?? false; $ppage = $args['ppage'] ?? false; // Determine if this particular report gets sample data, or if not if it's has been cached recently. $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 ); if ( false === $this->donors ) { // No cache? Let's run the data. $having_sql = ''; $order_by = ''; $where = ''; // generate mysql LIMIT and OFFSET. if ( ! $offset && $ppage && $limit ) { $offset = ( $ppage - 1 ) * $limit; } $limit_sql = ( $limit ) ? 'LIMIT ' . $limit : ''; $offset_sql = ( $offset ) ? 'OFFSET ' . $offset : ''; switch ( $report_type ) { case 'donors-recurring': $having_sql = 'HAVING total_count_donations > 1'; $order_by = 'ORDER BY total_amount DESC'; $query = "SELECT SUM( ccd2.amount ) AS total_amount, AVG( ccd2.amount ) AS average, COUNT( ccd2.donation_id ) AS total_count_donations, COUNT( DISTINCT( ccd2.campaign_id ) ) AS total_count_campaigns, last_donation.post_date AS last_donation_date, last_campaign.post_title AS last_campaign_title, last_campaign.ID AS last_campaign_id, 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 FROM {$wpdb->prefix}charitable_donors cd 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 FROM {$wpdb->prefix}charitable_campaign_donations 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 ) ccd ON ccd.donor_id = cd.donor_id JOIN ( SELECT donor_id, donation_id, campaign_id, amount, campaign_donation_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') ) ccd2 ON ccd2.donor_id = cd.donor_id INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_donation ON ccd.donation_id = last_donation.ID INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_campaign ON ccd.campaign_id = last_campaign.ID {$where} GROUP BY cd.donor_id {$having_sql} {$order_by} "; $this->donors = $wpdb->get_results( // phpcs:ignore $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore ); break; case 'donors-first-time': $having_sql = 'HAVING total_count_donations = 1'; $order_by = 'ORDER BY last_donation_date ASC'; $query = "SELECT ccd.amount AS total_amount, COUNT( ccd.donation_id ) AS total_count_donations, last_donation.post_date AS last_donation_date, last_campaign.post_title AS last_campaign_title, last_campaign.ID AS last_campaign_id, 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 FROM {$wpdb->prefix}charitable_donors cd JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON cd.donor_id = ccd.donor_id 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 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 {$where} GROUP BY cd.donor_id {$having_sql} {$order_by} "; $this->donors = $wpdb->get_results( // phpcs:ignore $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore ); break; default: $order_by = 'ORDER BY total_amount DESC'; $query = "SELECT SUM( ccd2.amount ) AS total_amount, AVG( ccd2.amount ) AS average, COUNT( ccd2.donation_id ) AS total_count_donations, COUNT( DISTINCT( ccd2.campaign_id ) ) AS total_count_campaigns, last_donation.post_date AS last_donation_date, last_campaign.post_title AS last_campaign_title, last_campaign.ID AS last_campaign_id, 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 FROM {$wpdb->prefix}charitable_donors cd 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 FROM {$wpdb->prefix}charitable_campaign_donations 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 ) ccd ON ccd.donor_id = cd.donor_id JOIN ( SELECT donor_id, donation_id, campaign_id, amount, campaign_donation_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') ) ccd2 ON ccd2.donor_id = cd.donor_id INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_donation ON ccd.donation_id = last_donation.ID INNER JOIN ( SELECT ID, post_title, post_date FROM {$wpdb->prefix}posts ) last_campaign ON ccd.campaign_id = last_campaign.ID {$where} GROUP BY cd.donor_id {$having_sql} {$order_by}"; $this->donors = $wpdb->get_results( // phpcs:ignore $query . ' ' . $limit_sql . ' ' . $offset_sql // phpcs:ignore ); break; } $total_query = 'SELECT COUNT(1) FROM (' . $query . ') AS combined_table'; $this->donors_total = $wpdb->get_var( $total_query ); // phpcs:ignore // Save this to the cache. // $this->set_cached_report( $report_type, $start_date, $end_date, false, $campaign_id, $category_id, $this->donors );. return $this->donors; } } /** * This accepts donations and attempts to create data for the headline chart with donation data. * * @param array $donations An array of donations. * @param string $start_date The start date. * @param string $end_date The end date. * * @since 1.8.1 */ public function init_axis_with_donations( $donations = array(), $start_date = false, $end_date = false ) { if ( false === $donations ) { return; } $date_data = array(); $donation_data = array(); foreach ( $donations as $donation ) { $donation_day = gmdate( 'M d', strtotime( $donation->post_date ) ); if ( ! in_array( $donation_day, $date_data, true ) ) { $date_data[] = $donation_day; } if ( isset( $donation_data[ $donation_day ]['amount'] ) ) { $donation_data[ $donation_day ]['amount'] += $donation->total_amount; } else { $donation_data[ $donation_day ]['amount'] = $donation->total_amount; } } $this->set_date_axis( $date_data, $start_date, $end_date ); $this->set_donation_axis( $donation_data, $start_date, $end_date ); } /** * This generates the donation axis for the headline chart. * * @param array $donation_data An array of donations. * @param string $start_date The start date. * @param string $end_date The end date. * * @since 1.8.1 */ public function set_donation_axis( $donation_data, $start_date, $end_date ) { $this->donation_axis = array(); $days = $this->get_days_between_dates( $start_date, $end_date ); if ( ! $days || empty( $days ) ) { return; } foreach ( $days as $day ) { $this->donation_axis[ $day ] = ! empty( $donation_data[ $day ]['amount'] ) ? $donation_data[ $day ]['amount'] : 0; } // Remove the keys from the array but keep the order of the values. $this->donation_axis = array_values( $this->donation_axis ); // 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. if ( count( $days ) === 1 ) { $this->donation_axis = array_merge( array( 0 ), $this->donation_axis, array( 0 ) ); } return $this->donation_axis; } /** * This generates the date axis for the headline chart. * * @param array $date_data An array of dates. * @param string $start_date The start date. * @param string $end_date The end date. * * @since 1.8.1 */ public function set_date_axis( $date_data, $start_date, $end_date ) { $days = $this->get_days_between_dates( $start_date, $end_date, false ); // 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. if ( count( $days ) === 1 ) { $days = array_merge( array( gmdate( 'M d', strtotime( $start_date . ' -1 day' ) ) ), $days, array( gmdate( 'M d', strtotime( $end_date . ' +1 day' ) ) ) ); } $this->date_axis = $days; return $days; } /** * Util function that gets dates between two given dates. * * @param string $start_date The start date. * @param string $end_date The end date. * @param string $array_key_format The format of the array key. * * @since 1.8.1 * * @return mixed */ public function get_days_between_dates( $start_date, $end_date, $array_key_format = 'Y-m-d' ) { // phpcs:ignore $days = array(); $current_date = new DateTime( $start_date ); $end_date = new DateTime( $end_date ); while ( $current_date <= $end_date ) { if ( $array_key_format ) : $days[ $current_date->format( $array_key_format ) ] = $current_date->format( 'M d' ); else : $days[] = $current_date->format( 'M d' ); endif; $current_date->modify( '+1 day' ); } return $days; } /** * This accepts a report type and date and attemps to create a refund "report". * * @since 1.8.1 * @version 1.8.8.6 * * @param string $campaign_id The campaign ID. * @param string $limit The limit of refunds to get. * * @return array */ public function get_refunds( $campaign_id = false, $limit = false ) { // phpcs:ignore global $wpdb; $where_sql = array(); $prepare_args = array(); $where_sql[] = 'p.post_status = %s'; $prepare_args[] = 'charitable-refunded'; // add to $where_sql depending on the values of $start_date and $end_date. if ( $this->start_date ) { $where_sql[] = 'p.post_date >= %s'; $prepare_args[] = $this->start_date . ' 00:00:00'; } if ( $this->end_date ) { $where_sql[] = 'p.post_date <= %s'; $prepare_args[] = $this->end_date . ' 23:59:59'; } $where_sql[] = 'pm1.meta_key = %s'; $prepare_args[] = 'donation_gateway'; $where_clause = implode( ' AND ', $where_sql ); // Build prepare args with post_type first, then where clause args. $all_prepare_args = array_merge( array( 'donation' ), $prepare_args ); // get all donations (posts with a post type of donation) between a start date and an end date. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter // $where_clause is built with placeholders (%s) that are properly processed by $wpdb->prepare(). // The interpolation is safe because all values in $where_clause are placeholders, not raw values. $refunded_donations = $wpdb->get_results( $wpdb->prepare( "SELECT SUM(cd.amount) AS total_amount, COUNT(p.ID) AS total_number_of_refunds, cd.donation_id AS donation_id, cd.campaign_id AS campaign_id, cd.donor_id AS donor_id, cd.amount AS amount, pm1.meta_value AS payment_gateway, p.post_date AS post_date, p.post_date_gmt AS post_date_gmt FROM {$wpdb->posts} p LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id LEFT JOIN {$wpdb->prefix}postmeta pm1 ON p.ID = pm1.post_id WHERE p.post_type = %s AND {$where_clause} GROUP BY p.ID", ...$all_prepare_args ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter $refunds_by_day = array(); $refunded_donations_total_amount = 0; $refunded_donations_total_count = 0; $processed_donation_ids = array(); // Track unique donation IDs to avoid double-counting $days = $this->get_days_between_dates( $this->start_date, $this->end_date ); // First, calculate totals from all refunded donations (regardless of day matching) // This ensures count and amount are consistent if ( ! empty( $refunded_donations ) ) { foreach ( $refunded_donations as $refunded_donation ) { // Only count each unique donation ID once $donation_id = isset( $refunded_donation->donation_id ) ? intval( $refunded_donation->donation_id ) : 0; if ( $donation_id > 0 && ! in_array( $donation_id, $processed_donation_ids, true ) ) { $refunded_donations_total_amount += floatval( $refunded_donation->amount ); $refunded_donations_total_count++; $processed_donation_ids[] = $donation_id; } } } // Then, organize by day for the breakdown foreach ( $days as $day ) { $donation_day = gmdate( 'Y-m-d', strtotime( $day ) ); $refunded_donation_amount = 0; $refunded_donation_count = 0; $processed_day_donation_ids = array(); // Track unique donation IDs per day if ( ! empty( $refunded_donations ) ) : foreach ( $refunded_donations as $refunded_donation ) { // if the refunded donation post date is the same day as the donation_day, add it to the refunded total. $refund_date = gmdate( 'Y-m-d', strtotime( $refunded_donation->post_date_gmt ) ); if ( $refund_date === $donation_day ) { $donation_id = isset( $refunded_donation->donation_id ) ? intval( $refunded_donation->donation_id ) : 0; // Only count each unique donation ID once per day if ( $donation_id > 0 && ! in_array( $donation_id, $processed_day_donation_ids, true ) ) { $refunded_donation_amount += floatval( $refunded_donation->amount ); ++$refunded_donation_count; $processed_day_donation_ids[] = $donation_id; } } } endif; $refunds_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day ) ); $refunds_by_day[ $donation_day ]['amount'] = $refunded_donation_amount; $refunds_by_day[ $donation_day ]['donors'] = $refunded_donation_count; } return array( 'donations' => $refunded_donations, 'total_amount' => $refunded_donations_total_amount, 'total_count' => $refunded_donations_total_count, 'refunds_by_day' => $refunds_by_day, ); } /** * This accepts a report type and date and attemps to create a report that has multiple data points. * * @param string $start_date The start date. * @param string $end_date The end date. * * @since 1.8.1 */ public function get_donation_breakdown_data( $start_date = false, $end_date = false ) { // phpcs:ignore global $wpdb; // Get all posts with a custom post type of 'donation' that were created on and including between $start_data and $end_date. $donations = $wpdb->get_results( // phpcs:ignore $wpdb->prepare( "SELECT SUM(cd.amount) AS total_amount, p.ID FROM $wpdb->posts p LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id WHERE p.post_type = 'donation' AND p.post_status = 'charitable-completed' GROUP BY p.ID" ) // phpcs:ignore ); // 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. $donations_by_day = $wpdb->get_results( // phpcs:ignore $wpdb->prepare( "SELECT SUM(cd.amount) AS total_amount, COUNT(p.ID) AS total_donors, DATE(p.post_date) AS post_date FROM $wpdb->posts p LEFT JOIN {$wpdb->prefix}charitable_campaign_donations cd ON p.ID = cd.donation_id WHERE p.post_type = 'donation' AND p.post_status = 'charitable-completed' GROUP BY DATE(p.post_date)" ) // phpcs:ignore ); } /** * This gathers info from the database with a focus on donors. * * @param string $slug Slug. * @param string $start_date The start date. * @param string $end_date The end date. * @param string $return_as The return type. Object or array. Default is object. * * @since 1.8.1 */ public function get_donors_from_db( $slug = 'compare_from', $start_date = false, $end_date = false, $return_as = 'object' ) { // what we need: // // Total Donation count between start_date and end_date. // Total Donation amount between start_date and end_date. global $wpdb; $return_as = ( 'array' === $return_as ) ? 'ARRAY_A' : 'OBJECT_K'; return ( $wpdb->get_results( // phpcs:ignore $wpdb->prepare( "SELECT cd.donor_id AS donor_id, COUNT(ccd.donation_id) as %s, SUM(ccd.amount) AS %s, cd.user_id AS user_id, cd.email AS donor_email, cd.first_name AS donor_first_name, cd.last_name AS donor_last_name FROM {$wpdb->prefix}charitable_donors cd LEFT JOIN {$wpdb->prefix}charitable_campaign_donations ccd ON ccd.donor_id = cd.donor_id LEFT JOIN {$wpdb->prefix}posts p ON p.ID = ccd.donation_id WHERE p.post_date >= %s AND p.post_date <= %s AND p.post_status = 'charitable-completed' GROUP BY cd.donor_id", 'total_donation_count_' . $slug, 'total_donation_amount_' . $slug, $start_date, $end_date ), $return_as ) ); } /** * Generates HTML for almost the entire activities area. * * @since 1.8.1 * * @param string $report_type The report type. * @param array $advanced_report_data The report data. * @param array $report_args The report args. * @param bool $show_actions Whether to show actions. * * @return string */ public function generate_advanced_breakdown_report_html( $report_type = 'lybunt', $advanced_report_data = array(), $report_args = array(), $show_actions = false ) { $table_meta = (array) $this->get_advanced_report_table_meta( $report_type, $report_args, $show_actions ); // Allow certain HTML tags in the column titles. $title_whitelist_tags = array( 'br' => array(), ); ob_start(); ?>
    generate_advanced_cards( $advanced_report_data, $report_type, $report_args ); // phpcs:ignore ?>


    $column_title ) : if ( $column_slug === 'avatar' ) { echo ''; } else { echo ''; } endforeach; endif; ?> generate_advanced_breakdown_rows( $advanced_report_data, $report_type, count( $table_meta['columns'], $show_actions ) ); // phpcs:ignore ?> $column_title ) : if ( $column_slug === 'avatar' ) { echo ''; } else { echo ''; } endforeach; ?>
    ' . wp_kses( $column_title, $title_whitelist_tags ) . '
    ' . wp_kses( $column_title, $title_whitelist_tags ) . '
    generate_lybunt_rows( $advanced_report_data, $number_of_columns, $show_actions ); break; default: // code... break; } return $rows; } /** * Generates HTML for the "report cards" at the top of the advanced page. * * @since 1.8.1 * * @param array $advanced_report_data The report data. * @param string $report_type The report type. * @param array $report_args The report args. * * @return string */ public function generate_advanced_cards( $advanced_report_data = array(), $report_type = 'lybunt', $report_args = array() ) { ob_start(); if ( 'lybunt' === $report_type ) : if ( charitable_is_pro() ) : if ( ! is_array( $advanced_report_data ) ) { $advanced_report_data = array(); } // count all elements in a multi-dimenstional array that has a particular value greator than 0. $total_count_donors_from = number_format( count( array_filter( array_column( $advanced_report_data, 'total_donation_count_compare_from' ), function ( $value ) { return $value > 0; } ) ) ); $total_count_donors_to = number_format( count( array_filter( array_column( $advanced_report_data, 'total_donation_count_compare_to' ), function ( $value ) { return $value > 0; } ) ) ); $total_amount_donations_from = array_sum( array_column( $advanced_report_data, 'total_donation_amount_compare_from' ) ); $total_amount_donations_to = array_sum( array_column( $advanced_report_data, 'total_donation_amount_compare_to' ) ); // if value does not have decimals, then add two decimal places. $total_amount_donations_from = ( false === strpos( $total_amount_donations_from, '.' ) ) ? $total_amount_donations_from . '.00' : $total_amount_donations_from; $total_amount_donations_to = ( false === strpos( $total_amount_donations_to, '.' ) ) ? $total_amount_donations_to . '.00' : $total_amount_donations_to; $from_date_range = '( ' . $report_args['start_date_compare_from'] . ' - ' . $report_args['end_date_compare_from'] . ' )'; $to_date_range = '( ' . $report_args['start_date_compare_to'] . ' - ' . $report_args['end_date_compare_to'] . ' )'; else : $total_count_donors_from = number_format( 143 ); $total_count_donors_to = number_format( 1332 ); $total_amount_donations_from = '$500.00'; $total_amount_donations_to = '$25,040.00'; $from_date_range = '2022/01/01 - 2022/12/31'; $to_date_range = '2023/01/01 - 2023/12/31'; endif; ?>

    esc_html__( 'Donors', 'charitable' ), 'columns' => array( 'avatar' => '', 'name-email' => esc_html__( 'Name', 'charitable' ), 'total-donations' => esc_html__( 'Last Donation', 'charitable' ), 'number-donations-1' => esc_html__( 'Total Donations', 'charitable' ) . '
    ' . $number_donations_1_date_range, 'number-donations-2' => esc_html__( 'Total Donations', 'charitable' ) . '
    ' . $number_donations_2_date_range, 'last-campaign' => esc_html__( 'Last Campaign', 'charitable' ), 'life-total' => esc_html__( 'Life Total', 'charitable' ), ), ); if ( $show_actions ) { $table_meta['columns']['actions'] = esc_html__( 'Actions', 'charitable' ); } break; default: // code... break; } return $table_meta; } /** * This gets report args and an iniital report. * * @param string $report_args The report args. * * @since 1.8.1 * * @return array */ public function get_lybunt_data( $report_args = array() ) { global $wpdb; $start_date_compare_from = esc_html( $report_args['start_date_compare_from'] ); $end_date_compare_from = esc_html( $report_args['end_date_compare_from'] ); $start_date_compare_to = esc_html( $report_args['start_date_compare_to'] ); $end_date_compare_to = esc_html( $report_args['end_date_compare_to'] ); $limit = ! empty( $report_args['limit'] ) ? intval( $report_args['limit'] ) : apply_filters( 'charitable_report_advanced_lybunt_limit', 100 ); $order_by = ! empty( $report_args['order_by'] ) ? esc_html( $report_args['order_by'] ) : 'total_life_amount'; $order = ! empty( $report_args['order'] ) ? esc_html( $report_args['order'] ) : 'DESC'; $_donors = array(); // Determine if this particular report has been cached recently. // For now though we are not caching. $donors = $this->maybe_cache_report( false ) ? false : $this->get_cached_report( 'lybunt', $start_date_compare_from, $end_date_compare_to, false, false, false ); if ( false === $donors ) { // Get the foundation of the report. $donors_compare_from = (array) $this->get_donors_from_db( 'compare_from', $start_date_compare_from, $end_date_compare_from, 'array' ); $donors_compare_to = (array) $this->get_donors_from_db( 'compare_to', $start_date_compare_to, $end_date_compare_to, 'array' ); // Merge the two arrays - from and to. if ( ! empty( $donors_compare_from ) ) : foreach ( $donors_compare_from as $key => $donor ) : $_donors[ $donor['donor_id'] ] = $donor; endforeach; endif; if ( ! empty( $donors_compare_to ) ) : foreach ( $donors_compare_to as $key => $donor ) : // if donor_id exists in $_donors, merge the two arrays. if ( is_array( $_donors ) && array_key_exists( $donor['donor_id'], $_donors ) ) { $_donors[ $donor['donor_id'] ] = array_merge( $_donors[ $donor['donor_id'] ], $donor ); } else { $_donors[ $donor['donor_id'] ] = $donor; } endforeach; endif; if ( empty( $_donors ) ) { return false; } // Convert array of arrays to array of objects, keeping the array keys. $_donors = array_map( function ( $donor ) { return (object) $donor; }, $_donors ); $donors = array(); foreach ( $_donors as $key => $donor ) { if ( ! empty( $donor->donor_id ) && 0 !== intval( $donor->donor_id ) ) : $donors[ $donor->donor_id ] = $donor; endif; } foreach ( $donors as $donor_id => $donor_info ) { $donor = new Charitable_Donor( $donor_id ); $last_donation = $donor->get_last_donation(); $last_donation_amount = $last_donation->get_total_donation_amount(); $last_donation_campaign_links = $last_donation->get_campaigns_links(); $donors[ $donor_id ]->last_donation_amount = $last_donation_amount; $donors[ $donor_id ]->last_donation_campaign_links = $last_donation_campaign_links; $donors[ $donor_id ]->total_life_amount = $donor->get_amount(); if ( empty( $donors[ $donor_id ]->total_donation_count_compare_from ) ) { $donors[ $donor_id ]->total_donation_count_compare_from = 0; } if ( empty( $donors[ $donor_id ]->total_donation_amount_compare_from ) ) { $donors[ $donor_id ]->total_donation_amount_compare_from = 0; } if ( empty( $donors[ $donor_id ]->total_donation_count_compare_to ) ) { $donors[ $donor_id ]->total_donation_count_compare_to = 0; } if ( empty( $donors[ $donor_id ]->total_donation_amount_compare_to ) ) { $donors[ $donor_id ]->total_donation_amount_compare_to = 0; } } // Sort the data. if ( is_array( $donors ) && count( $donors ) > 0 ) : $donors = $this->sort_lybunt_data( $donors, $order_by, $order ); endif; // Reduce the array to the limit. if ( $limit && is_array( $donors ) && count( $donors ) > $limit ) : $donors = array_slice( $donors, 0, $limit, true ); endif; // Save this to the cache. $this->set_cached_report( 'lybunt', $start_date_compare_from, $end_date_compare_to, false, false, false, $donors ); } return $donors; } /** * Sorts the lybunt data. * * @since 1.8.1.3 * * @param array $donors The donors data. * @param string $order_by The order by. Default is total_life_amount. * @param string $order The order (DESC OR ASC). Default is DESC. * * @return array */ public function sort_lybunt_data( $donors = array(), $order_by = 'total_life_amount', $order = 'DESC' ) { if ( ! empty( $donors ) ) : usort( $donors, function ( $a, $b ) use ( $order_by, $order ) { if ( $order === 'DESC' && ! empty( $a->$order_by ) && ! empty( $b->$order_by ) ) { return $b->$order_by <=> $a->$order_by; } elseif ( $order === 'ASC' && ! empty( $a->$order_by ) && ! empty( $b->$order_by ) ) { return $a->$order_by <=> $b->$order_by; } else { return; } } ); endif; return $donors; } /** * Generates HTML rows for doantion breakdown table. * * @since 1.8.1 * * @param array $donors_data The donors data. * @param int $number_of_columns The number of columns. * @param bool $show_actions Whether to show actions. * * @return string */ public function generate_lybunt_rows( $donors_data = array(), $number_of_columns = false, $show_actions = false ) { ob_start(); if ( empty( $donors_data ) ) { ?> $data ) : $donor_name = $data->donor_first_name . ' ' . $data->donor_last_name; $donor_email = $data->donor_email; $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . '/images/misc/placeholder-avatar-small.jpg'; $total_donation_count_compare_to = $data->total_donation_count_compare_to; $total_donation_amount_compare_to = number_format( $data->total_donation_amount_compare_to, 2 ); $total_donation_count_compare_from = $data->total_donation_count_compare_from; $total_donation_amount_compare_from = number_format( $data->total_donation_amount_compare_from, 2 ); $last_donation_amount = $data->last_donation_amount; $total_life_amount = number_format( $data->total_life_amount, 2 ); $campaign_links = implode( ',', $data->last_donation_campaign_links ); $link = false; $actions_display = ''; ?> <?php esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>


    ' . esc_html__( 'View User', 'charitable' ) . ''; else : $actions_display = '' . esc_html__( 'View User', 'charitable' ) . ''; endif; endif; echo $actions_display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> __( 'LYBUNT / SYBUNT', 'charitable' ), ) ); ob_start(); ?> get_lybunt_data( $report_args ) ); break; // phpcs:ignore default: return false; break; // phpcs:ignore } } /** * Return net value * * @since 1.8.1 * * @param float $donation_amount The donation amount. * @param float $refund_amount The refund amount. * * @return float */ public function get_net( $donation_amount = 0, $refund_amount = 0 ) { // If the donation amount is less than the refund amount, return 0. if ( $donation_amount < $refund_amount ) { return 0; } return $donation_amount - $refund_amount; } /** * Amount formatting for reports - includes currency symbol. * * @since 1.8.1 * * @param int $amount The amount. * * @return string */ public function charitable_reports_format_money( $amount = 0 ) { return html_entity_decode( charitable_format_money( $amount ) ); } /** * Returns if activities are empty. * * @since 1.8.1 * * @return boolean */ public function is_activities_empty() { if ( $this->activities === false || empty( $this->activities ) ) { return true; } return false; } /** * Get localized strings. * * @since 1.8.1 * * @return array */ private function get_localized_strings() { $charitable_reports_advanced_report_strings = apply_filters( 'charitable_reports_advanced_report_strings', array( 'lybunt' => array( 'title' => __( 'LYBUNT / SYBUNT', 'charitable' ), 'description' => wp_kses_post( __( '

    LYBUNT stands for Last Year But Unfortunately Not This Year, meaning the donor supported your organization during the last fiscal year but did not return to donate again this year. SYBUNT stands for Some Year But Unfortunately Not This Year.

    Why This Is Important: 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.

    ', 'charitable' ), array( 'p' => array(), 'strong' => array(), ) ), ), ) ); $currency_helper = charitable_get_currency_helper(); // Decode HTML entities in currency symbol to prevent double-encoding in JSON. // The currency symbol comes as HTML entities (e.g., $ for $), so we decode it // before passing to JavaScript to avoid double-encoding issues. // This ensures the currency symbol is a plain character that won't be double-encoded. $currency_symbol = $currency_helper->get_currency_symbol(); // Decode HTML entities to get the plain character (e.g., $ becomes $). $currency_symbol = html_entity_decode( $currency_symbol, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $decimal_count = $currency_helper->get_decimals(); $strings = array( 'version' => Charitable::VERSION, 'nonce' => wp_create_nonce( 'charitable-reporting' ), 'admin_nonce' => wp_create_nonce( 'charitable-admin' ), 'currency_symbol' => $currency_symbol, 'decimal_count' => (int) $decimal_count, 'ajax_url' => admin_url( 'admin-ajax.php' ), 'date_select_day' => 'DD', 'date_select_month' => 'MM', 'headline_chart_options' => array( 'donation_axis' => (array) $this->donation_axis, 'date_axis' => (array) $this->date_axis, ), 'payment_methods_chart_options' => array( 'payment_percentages' => (array) array_reverse( $this->payment_percentages ), 'payment_labels' => (array) array_reverse( $this->payment_labels ), 'payment_keys' => (array) array_reverse( $this->payment_keys ), ), 'default_start_date' => current_time( gmdate( 'Y-m-d', strtotime( '-1 month' ) ) ), 'default_end_date' => current_time( gmdate( 'Y-m-d' ) ), 'advanced_reports' => $charitable_reports_advanced_report_strings, ); $strings = apply_filters( 'charitable_reporting_strings', $strings ); return $strings; } /** * Return the array of tabs used on the settings page. * * @since 1.8.1 * * @return string[] */ public function get_sections() { /** * Filter the settings tabs. * * @since 1.8.1 * * @param string[] $tabs List of tabs in key=>label format. */ return apply_filters( 'charitable_reports_tabs', array( 'overview' => __( 'Overview', 'charitable' ), 'advanced' => __( 'Advanced', 'charitable' ), 'activity' => __( 'Activity', 'charitable' ), 'donors' => __( 'Donors', 'charitable' ), 'analytics' => __( 'Analytics', 'charitable' ), ) ); } /** * Return the array of terms for custom campaign categories. * * @since 1.8.1 * * @return string[] */ public function get_campaign_categories() { return apply_filters( 'charitable_reports_filter_campaign_categories', get_terms( 'campaign_category' ) ); } /** * Provides the content (title/description) to display inside the title card for a particular report. * * @since 1.8.1 * * @param string $report_type The report type. * * @return string[] */ public function generate_title_card_html( $report_type = false ) { if ( false === $report_type ) { return; } switch ( sanitize_title( $report_type ) ) { case 'donors-top': $title = __( 'Top Donors', 'charitable' ); // use wp_kses_post to allow html in the string. $description = wp_kses_post( __( '

    This report shows which donors have made the largest gifts and donations.

    Why This Is Important: 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.

    You can also use this report to thank top donors for their support.

    ', 'charitable' ), array( 'p' => array(), 'strong' => array(), ) ); break; case 'donors-recurring': $title = __( 'Recurring Donors', 'charitable' ); // use wp_kses_post to allow html in the string. $description = wp_kses_post( __( '

    This report shows which donors have made repeated donations the largest gifts and donations.

    Why This Is Important: 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.

    You can also use this report to thank recurring donors for their support.

    ', 'charitable' ), array( 'p' => array(), 'strong' => array(), ) ); break; case 'donors-first-time': $title = __( 'First Time Donors', 'charitable' ); // use wp_kses_post to allow html in the string. $description = wp_kses_post( __( '

    This report shows which donors have made one donation - their first.

    Why This Is Important: Seeing which donors are new to your campaigns can help you determine where your traffic is coming from or how donors are finding you.

    You can also use this report to thank first time donors for their support.

    ', 'charitable' ), array( 'p' => array(), 'strong' => array(), ) ); break; case 'lybunt': $title = __( 'LYBUNT / SYBUNT', 'charitable' ); $description = wp_kses_post( __( '

    LYBUNT stands for Last Year But Unfortunately Not This Year, meaning the donor supported your organization during the last fiscal year but did not return to donate again this year. SYBUNT stands for Some Year But Unfortunately Not This Year.

    Why This Is Important: 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.

    ', 'charitable' ), array( 'p' => array(), 'strong' => array(), ) ); break; default: // code... break; } ob_start(); ?>

    date_filter_values; ob_start(); ?> is_report_type_donor( $report_type ) ) { for ( $x = 0; $x < $number_of_records; $x++ ) { if ( $data_type === 'object' ) { $random_price = wp_rand( 50, 10000 ) / 10; $data->{$x} = new stdClass(); $data->{$x}->total_amount = $random_price; $data->{$x}->average = $random_price; $data->{$x}->total_count_donations = 1; $data->{$x}->total_count_campaigns = 1; $data->{$x}->last_donation_date = '2023-11-28 16:59:33'; $data->{$x}->last_campaign_title = 'My New Campaign 1'; $data->{$x}->last_campaign_id = 0; $data->{$x}->donor_id = 0; $data->{$x}->user_id = 0; $data->{$x}->email = 'example_donor_' . $x . '@wpcharitable.com'; $data->{$x}->first_name = 'Roger'; $data->{$x}->last_name = 'Wilco'; $data->{$x}->date_joined = '2023-11-28 16:59:33'; $data->{$x}->donation_id = 0; $data->{$x}->campaign_id = 0; $data->{$x}->amount = $random_price; $data->{$x}->sample = true; } else { $random_price_1 = mt_rand( 50, 1000 ) / 10; // phpcs:ignore $data[] = array( 'total_amount' => $random_price_1, 'average' => $random_price_1, 'total_count_donations' => 1, 'total_count_campaigns' => 1, 'last_donation_date' => '2023-11-28 16:59:33', 'last_campaign_title' => 'My New Campaign 1', 'last_campaign_id' => 0, 'donor_id' => 0, 'user_id' => 0, 'email' => 'example_donor_' . $x . '@wpcharitable.com', 'first_name' => 'Roger', 'last_name' => 'Wilco', 'date_joined' => '2024-01-28 16:59:33', 'donation_id' => 0, 'campaign_id' => 0, 'amount' => $random_price_1, 'sample' => true, ); } } } elseif ( $report_type === 'advanced' ) { $random_price_1 = wp_rand( 50, 10000 ) / 10; $random_price_2 = wp_rand( 50, 10000 ) / 10; $random_price_3 = wp_rand( 50, 10000 ) / 10; $random_price_4 = wp_rand( 50, 10000 ) / 10; $data = array( 1 => (object) array( 'donor_id' => 1, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_1, 'user_id' => 0, 'donor_email' => 'dbisset@donoremail.com', 'donor_first_name' => 'David', 'donor_last_name' => 'Bisset', 'last_donation_amount' => $random_price_2, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => 678, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 2 => (object) array( 'donor_id' => 2, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_3, 'user_id' => 0, 'donor_email' => 'sarah@donoremail.com', 'donor_first_name' => 'Sarah', 'donor_last_name' => 'Smith', 'last_donation_amount' => $random_price_4, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => $random_price_3, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 3 => (object) array( 'donor_id' => 1, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_1, 'user_id' => 0, 'donor_email' => 'jim@donoremail.com', 'donor_first_name' => 'Jim', 'donor_last_name' => 'Bisset', 'last_donation_amount' => $random_price_2, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => 678, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 4 => (object) array( 'donor_id' => 2, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_3, 'user_id' => 0, 'donor_email' => 'george@donoremail.com', 'donor_first_name' => 'George', 'donor_last_name' => 'Smith', 'last_donation_amount' => $random_price_4, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => $random_price_3, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 5 => (object) array( 'donor_id' => 1, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_1, 'user_id' => 0, 'donor_email' => 'zoe123@donoremail.com', 'donor_first_name' => 'Zoe', 'donor_last_name' => 'Bisset', 'last_donation_amount' => $random_price_2, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => 678, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 6 => (object) array( 'donor_id' => 2, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_3, 'user_id' => 0, 'donor_email' => 'avas@donoremail.com', 'donor_first_name' => 'Ava', 'donor_last_name' => 'Smith', 'last_donation_amount' => $random_price_4, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => $random_price_3, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 7 => (object) array( 'donor_id' => 1, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_1, 'user_id' => 0, 'donor_email' => 'zoe123@donoremail.com', 'donor_first_name' => 'Zoe', 'donor_last_name' => 'Bisset', 'last_donation_amount' => $random_price_2, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => 678, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 8 => (object) array( 'donor_id' => 2, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_3, 'user_id' => 0, 'donor_email' => 'avas@donoremail.com', 'donor_first_name' => 'Ava', 'donor_last_name' => 'Smith', 'last_donation_amount' => $random_price_4, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => $random_price_3, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 9 => (object) array( 'donor_id' => 1, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_1, 'user_id' => 0, 'donor_email' => 'dbisset@donoremail.com', 'donor_first_name' => 'David', 'donor_last_name' => 'Bisset', 'last_donation_amount' => $random_price_2, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => 678, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), 10 => (object) array( 'donor_id' => 2, 'total_donation_count_compare_to' => 3, 'total_donation_amount_compare_to' => $random_price_3, 'user_id' => 0, 'donor_email' => 'sarah@donoremail.com', 'donor_first_name' => 'Sarah', 'donor_last_name' => 'Smith', 'last_donation_amount' => $random_price_4, 'last_donation_campaign_links' => array( 34 => 'Help the Children Campaign', ), 'total_life_amount' => $random_price_3, 'total_donation_count_compare_from' => 0, 'total_donation_amount_compare_from' => 0, 'sample' => true, ), ); } $data = apply_filters( 'charitable_report_donation_sample_data', $data, $report_type, $data_type, $number_of_records ); return $data; } /** * Determine if the report type is a particular type of donor report. * * @since 1.8.1 * * @param string $report_type The report type. * * @return string */ public function is_report_type_donor( $report_type = false ) { return ( strpos( $report_type, 'donors-' ) === 0 ); } /** * Checks and sees if the string is a valid date, and if so return it in Y-m-d format. * * @since 1.8.1 * * @param string $date_string The date string. * * @return string */ public function get_valid_date_string( $date_string = false ) { // Determine if $date_string is a valid date. $parsed_date = gmdate( 'Y/m/d', strtotime( $date_string ) ); $date = DateTime::createFromFormat( 'Y/m/d', $parsed_date ); if ( $date && $date->format( 'Y/m/d' ) === $parsed_date ) { return $date->format( 'Y-m-d' ); } else { return false; } } /** * Display upgrade notice at the bottom on the plugin settings pages. * * @since 1.8.1 * @version 1.8.8 * * @param string $view Current view inside the plugin settings page. * * @return void */ public function reports_cta( $view = false, $css_class = 'reports-lite-cta', $show_close_button = false ) { // phpcs:ignore if ( charitable_is_pro() ) { // no need to display this cta since they have a valid license. return; } if ( get_option( 'charitable_lite_reports_upgrade', false ) || apply_filters( 'charitable_lite_reports_upgrade', false ) ) { return; } ?> id === 'charitable_page_charitable-reports' && ( empty( $_GET['tab'] ) || ( ! empty( $_GET['tab'] ) && charitable_reports_allow_tab_load_scripts( strtolower( $_GET['tab'] ) ) ) ) ) ) { // phpcs:ignore // Specific styles for the "overview" tab. add_action( 'admin_footer', [ $this, 'report_vars' ], 100 ); } } /** * See if it's the right time to load cta. * * @since 1.8.1 * * @return void */ public function maybe_add_reports_cta() { add_action( 'charitable_report_before_donor_table', [ $this, 'reports_cta' ] ); add_action( 'charitable_report_before_activity_table', [ $this, 'reports_cta' ] ); add_action( 'charitable_report_before_advanced_table', [ $this, 'reports_cta' ] ); } /** * Returns and/or create the single instance of this class. * * @since 1.8.1 * * @return Charitable_Reports */ public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } } endif;