PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
← All changes | includes/admin/reports/class-charitable-reports.php +151 -57 1.8.11 → 1.8.13.1 View file →
@@ -458,9 +458,9 @@
458 458 }
459 459 $badge_css = strtolower( $donation_parent ) === 'recurring' ? 'recurring' : 'one-time';
460 460 $badge_label = strtolower( $donation_parent ) === 'recurring' ? esc_html__( 'Recurring', 'charitable' ) : esc_html__( 'One-Time', 'charitable' );
461 461
462 - $display_amount = ! empty( $top_donor->total_donation_amount ) ? charitable_format_money( $top_donor->total_donation_amount, 2, true ) : charitable_format_money( esc_html( $top_donor->amount ), 2, true );
462 + $display_amount = ! empty( $top_donor->total_donation_amount ) ? charitable_format_money( $top_donor->total_donation_amount, false, true ) : charitable_format_money( esc_html( $top_donor->amount ), false, true );
463 463 $display_count = ! empty( $top_donor->total_donation_count ) ? intval( $top_donor->total_donation_count ) : 1;
464 464
465 465 // based on display count, override the badge css/label.
466 466 if ( $display_count > 1 ) {
@@ -761,15 +761,15 @@
761 761 case 'charitable-failed':
762 762 $admin_campaign_url = ! empty( $activity->campaign_id ) ? charitable_get_admin_campaign_edit_url( $activity->campaign_id ) : '#';
763 763 $admin_donation_url = ! empty( $activity->item_id ) ? charitable_get_admin_donation_edit_url( $activity->item_id ) : false;
764 764 $campaign_title = ! empty( $activity->campaign_title ) ? ' to <a target="_blank" href="' . $admin_campaign_url . '"><span class="campaign-title">' . esc_html( $activity->campaign_title ) . '</span></a> ' : '';
765 - $secondary_info = $admin_donation_url ? '<p class="charitable-reports-activity-secondary-info amount"><a href="' . $admin_donation_url . '" target="_blank">' . charitable_format_money( $activity->amount, 2, true ) . '</a>' . $campaign_title . '</p>' : '<p class="charitable-reports-activity-secondary-info amount">' . charitable_format_money( $activity->amount, 2, true ) . $campaign_title . '</p>';
765 + $secondary_info = $admin_donation_url ? '<p class="charitable-reports-activity-secondary-info amount"><a href="' . $admin_donation_url . '" target="_blank">' . charitable_format_money( $activity->amount, false, true ) . '</a>' . $campaign_title . '</p>' : '<p class="charitable-reports-activity-secondary-info amount">' . charitable_format_money( $activity->amount, false, true ) . $campaign_title . '</p>';
766 766 break;
767 767 default:
768 768 $admin_campaign_url = ! empty( $activity->campaign_id ) ? charitable_get_admin_campaign_edit_url( $activity->campaign_id ) : '#';
769 769 $admin_donation_url = ! empty( $activity->item_id ) ? charitable_get_admin_donation_edit_url( $activity->item_id ) : false;
770 770 $campaign_title = ! empty( $activity->campaign_title ) ? ' to <a target="_blank" href="' . $admin_campaign_url . '"><span class="campaign-title">' . esc_html( $activity->campaign_title ) . '</span></a> ' : '';
771 - $secondary_info = $admin_donation_url ? '<p class="charitable-reports-activity-secondary-info amount"><a href="' . $admin_donation_url . '" target="_blank">' . charitable_format_money( $activity->amount, 2, true ) . '</a>' . $campaign_title . '</p>' : '<p class="charitable-reports-activity-secondary-info amount">' . charitable_format_money( $activity->amount, 2, true ) . $campaign_title . '</p>';
771 + $secondary_info = $admin_donation_url ? '<p class="charitable-reports-activity-secondary-info amount"><a href="' . $admin_donation_url . '" target="_blank">' . charitable_format_money( $activity->amount, false, true ) . '</a>' . $campaign_title . '</p>' : '<p class="charitable-reports-activity-secondary-info amount">' . charitable_format_money( $activity->amount, false, true ) . $campaign_title . '</p>';
772 772 break;
773 773 }
774 774
775 775 return $secondary_info;
@@ -1125,9 +1125,9 @@
1125 1125 foreach ( $donor_data as $index => $data ) :
1126 1126
1127 1127 $donor_id = isset( $data->donor_id ) ? intval( $data->donor_id ) : 0;
1128 1128 $donor_email = isset( $data->email ) ? esc_html( $data->email ) : '';
1129 - $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . '/images/misc/placeholder-avatar-small.jpg';
1129 + $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . 'images/misc/placeholder-avatar-small.jpg';
1130 1130 $donor_name = esc_html( $data->first_name ) . ' ' . esc_html( $data->last_name );
1131 1131 $donor_number_donations = isset( $data->total_count_donations ) ? intval( $data->total_count_donations ) : 0;
1132 1132 $donor_number_campaigns = isset( $data->total_count_campaigns ) ? intval( $data->total_count_campaigns ) : 0;
1133 1133 $donor_average = $donor_number_donations > 0 ? $data->total_amount / $donor_number_donations : 0;
@@ -1172,11 +1172,11 @@
1172 1172 } elseif ( $is_sample_data ) {
1173 1173 $last_donation_html = $last_donation_campaign_name;
1174 1174 }
1175 1175 } elseif ( $last_donation_date && $last_donation_campaign_name && $last_donation_campaign_id ) {
1176 - $last_donation_html = charitable_format_money( esc_html( $data->amount ), 2, true ) . '<br/><a href="' . $action_link . '" target="_blank">' . $last_donation_campaign_name . '</a><br/>' . $last_donation_date;
1176 + $last_donation_html = charitable_format_money( esc_html( $data->amount ), false, true ) . '<br/><a href="' . $action_link . '" target="_blank">' . $last_donation_campaign_name . '</a><br/>' . $last_donation_date;
1177 1177 } elseif ( $is_sample_data ) {
1178 - $last_donation_html = charitable_format_money( esc_html( $data->amount ), 2, true ) . '<br/>' . $last_donation_campaign_name . '<br/>' . $last_donation_date;
1178 + $last_donation_html = charitable_format_money( esc_html( $data->amount ), false, true ) . '<br/>' . $last_donation_campaign_name . '<br/>' . $last_donation_date;
1179 1179 }
1180 1180
1181 1181 if ( $report_type === 'donors-first-time' ) {
1182 1182 ?>
@@ -1183,15 +1183,15 @@
1183 1183
1184 1184 <tr id="type-<?php echo esc_attr( $report_type ); ?>-donor-<?php echo intval( $donor_id ); ?>" class="type-<?php echo esc_attr( $report_type ); ?>">
1185 1185 <td class="charitable-avatar" style="width: 32px;"><img src="<?php echo esc_url( $donor_avatar ); ?>" class="avatar avatar-32 photo charitable-avatar-donor-<?php echo intval( $donor_id ); ?>" width="32" height="32" alt="<?php echo esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>"></td>
1186 1186 <td class="donated column-date" data-colname="<?php echo esc_html__( 'Name / Email', 'charitable' ); ?>"><?php echo esc_html( $donor_name ); ?><br/><?php echo esc_html( $donor_email ); ?></td>
1187 - <td class="donated column-donation-amount" data-colname="<?php echo esc_html__( 'Donation Amount', 'charitable' ); ?>"><?php echo charitable_format_money( esc_html( $data->total_amount ), 2, true ); // phpcs:ignore ?></td>
1187 + <td class="donated column-donation-amount" data-colname="<?php echo esc_html__( 'Donation Amount', 'charitable' ); ?>"><?php echo charitable_format_money( esc_html( $data->total_amount ), false, true ); // phpcs:ignore ?></td>
1188 1188 <td class="donated column-campaign" data-colname="<?php echo esc_html__( 'Campaign', 'charitable' ); ?>"><?php echo $last_donation_html; // phpcs:ignore ?></td>
1189 1189 <td class="donated column-date" data-colname="<?php echo esc_html__( 'Date', 'charitable' ); ?>"><?php echo esc_html( $last_donation_date ); ?></td>
1190 1190 <?php
1191 1191 $display = '<td class="donated column-actions " data-colname="Actions">';
1192 1192 if ( $action_label !== false ) {
1193 - $display .= '<a class="charitable-campaign-action-button" title="' . $action_label . '" href="' . esc_url( $action_link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . '/images/icons/eye.svg" width="14" height="14" alt="' . $action_label . '" /></a>';
1193 + $display .= '<a class="charitable-campaign-action-button" title="' . $action_label . '" href="' . esc_url( $action_link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . 'images/icons/eye.svg" width="14" height="14" alt="' . $action_label . '" /></a>';
1194 1194 }
1195 1195 $display .= '</td>';
1196 1196
1197 1197 echo $display; // phpcs:ignore
@@ -1202,17 +1202,17 @@
1202 1202
1203 1203 <tr id="type-<?php echo esc_attr( $report_type ); ?>-donor-<?php echo intval( $donor_id ); ?>" class="type-<?php echo esc_attr( $report_type ); ?>">
1204 1204 <td class="charitable-avatar" style="width: 32px;"><img src="<?php echo esc_url( $donor_avatar ); ?>" class="avatar avatar-32 photo charitable-avatar-donor-<?php echo intval( $donor_id ); ?>" width="32" height="32" alt="<?php echo esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>"></td>
1205 1205 <td class="donated column-date" data-colname="<?php echo esc_html__( 'Name / Email', 'charitable' ); ?>"><?php echo esc_html( $donor_name ); ?><br/><?php echo esc_html( $donor_email ); ?></td>
1206 - <td class="donated column-total-donations" data-colname="<?php echo esc_html__( 'Total Donations', 'charitable' ); ?>"><?php echo charitable_format_money( esc_html( $data->total_amount ), 2, true ); // phpcs:ignore ?></td>
1206 + <td class="donated column-total-donations" data-colname="<?php echo esc_html__( 'Total Donations', 'charitable' ); ?>"><?php echo charitable_format_money( esc_html( $data->total_amount ), false, true ); // phpcs:ignore ?></td>
1207 1207 <td class="donated column-number-donations" data-colname="Number of Donations"><?php echo esc_html( $donor_number_donations ); ?></td>
1208 - <td class="donated column-average" data-colname="Average"><?php echo charitable_format_money( $donor_average, 2, true ); // phpcs:ignore ?></td>
1208 + <td class="donated column-average" data-colname="Average"><?php echo charitable_format_money( $donor_average, false, true ); // phpcs:ignore ?></td>
1209 1209 <td class="donated column-number-campaigns" data-colname="Number of Campaigns"><?php echo esc_html( $donor_number_campaigns ); ?></td>
1210 1210 <td class="donated column-last-donation" data-colname="Last Donation"><?php echo $last_donation_html; // phpcs:ignore ?></td>
1211 1211 <?php
1212 1212 $display = '<td class="donated column-actions" data-colname="Actions">';
1213 1213 if ( $action_label !== false ) {
1214 - $display .= '<a class="charitable-campaign-action-button" title="' . $action_label . '" href="' . esc_url( $action_link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . '/images/icons/eye.svg" width="14" height="14" alt="' . $action_label . '" /</a>';
1214 + $display .= '<a class="charitable-campaign-action-button" title="' . $action_label . '" href="' . esc_url( $action_link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . 'images/icons/eye.svg" width="14" height="14" alt="' . $action_label . '" /</a>';
1215 1215 }
1216 1216 $display .= '</td>';
1217 1217
1218 1218 echo $display; // phpcs:ignore
@@ -1443,18 +1443,34 @@
1443 1443 $donations_by_day = array();
1444 1444 $days = $this->get_days_between_dates( $this->start_date, $this->end_date );
1445 1445 $refunded_statuses = array( 'charitable-refunded' );
1446 1446
1447 + /**
1448 + * Two grouped queries for the whole range, rather than two queries per calendar
1449 + * day. The old per-day loop made the cost a function of how long the organisation
1450 + * had existed rather than how much data it had, so a 24-year range issued ~18,000
1451 + * queries and hit max_execution_time before returning anything.
1452 + *
1453 + * The range is taken from $days rather than from $this->start_date /
1454 + * $this->end_date, because get_days_between_dates() normalises a falsy date to
1455 + * "now". Reading the bounds back off $days guarantees the query covers exactly
1456 + * the days this loop is about to render, however those dates were normalised.
1457 + */
1458 + $day_keys = array_keys( $days );
1459 + $range_start = reset( $day_keys );
1460 + $range_end = end( $day_keys );
1461 +
1462 + $completed_by_day = $campaign_donations_table->get_donations_summary_by_day_range( $range_start, $range_end, array( 'charitable-completed', 'charitable-refunded' ) );
1463 + $refunded_by_day = $campaign_donations_table->get_donations_summary_by_day_range( $range_start, $range_end, $refunded_statuses );
1464 +
1447 1465 foreach ( $days as $day_unformatted => $day_label ) {
1448 1466
1449 - $donation_today_completed = $campaign_donations_table->get_donations_summary_by_period( gmdate( 'Y-m-d', strtotime( $day_unformatted ) ), array( 'charitable-completed', 'charitable-refunded' ) );
1450 - $donation_today_refunded = $campaign_donations_table->get_donations_summary_by_period( gmdate( 'Y-m-d', strtotime( $day_unformatted ) ), $refunded_statuses );
1467 + $donation_day = gmdate( 'Y-m-d', strtotime( $day_unformatted ) );
1451 1468
1452 - $donation_day = gmdate( 'Y-m-d', strtotime( $day_unformatted ) );
1453 1469 $donations_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day_unformatted ) );
1454 - $donations_by_day[ $donation_day ]['amount'] = $donation_today_completed->amount;
1470 + $donations_by_day[ $donation_day ]['amount'] = isset( $completed_by_day[ $donation_day ] ) ? $completed_by_day[ $donation_day ]->amount : 0;
1455 1471 $donations_by_day[ $donation_day ]['donors'] = 0;
1456 - $donations_by_day[ $donation_day ]['refunds'] = $donation_today_refunded->amount;
1472 + $donations_by_day[ $donation_day ]['refunds'] = isset( $refunded_by_day[ $donation_day ] ) ? $refunded_by_day[ $donation_day ]->amount : 0;
1457 1473 $donations_by_day[ $donation_day ]['net'] = 0;
1458 1474 }
1459 1475
1460 1476 // Go through the array and group the amounts and number of donors by day.
@@ -2831,50 +2847,58 @@
2831 2847
2832 2848 $refunds_by_day = array();
2833 2849 $refunded_donations_total_amount = 0;
2834 2850 $refunded_donations_total_count = 0;
2835 - $processed_donation_ids = array(); // Track unique donation IDs to avoid double-counting
2836 2851
2837 2852 $days = $this->get_days_between_dates( $this->start_date, $this->end_date );
2838 2853
2839 - // First, calculate totals from all refunded donations (regardless of day matching)
2840 - // This ensures count and amount are consistent
2854 + /**
2855 + * Bucket the refunds by day once, up front, then read from that.
2856 + *
2857 + * The day loop below used to iterate the day LABEL ('M d') rather than the
2858 + * 'Y-m-d' key, so strtotime() stamped every bucket with the CURRENT year. The
2859 + * Refunds column and Net in the breakdown table therefore read 0 for any range
2860 + * outside the current year, and for a multi-year range refunds from different
2861 + * years merged into a single month-day bucket. (The totals were already
2862 + * computed up front, so the Refunds card itself was correct.)
2863 + *
2864 + * Buckets on post_date, matching the WHERE clause above and the donation series
2865 + * in get_donations_by_day(). Bucketing on post_date_gmt while filtering on
2866 + * post_date could put a late-evening refund in a day outside the range on a
2867 + * non-UTC site.
2868 + *
2869 + * The GROUP BY p.ID in the query already guarantees one row per donation, so no
2870 + * per-day dedup is needed. The previous "$donation_id > 0" guard also silently
2871 + * dropped a refunded donation that has no campaign_donations row from the totals;
2872 + * such a refund is now counted, with a zero amount, so it stays visible.
2873 + */
2874 + $refunds_by_date = array();
2875 +
2841 2876 if ( ! empty( $refunded_donations ) ) {
2842 2877 foreach ( $refunded_donations as $refunded_donation ) {
2843 - // Only count each unique donation ID once
2844 - $donation_id = isset( $refunded_donation->donation_id ) ? intval( $refunded_donation->donation_id ) : 0;
2845 - if ( $donation_id > 0 && ! in_array( $donation_id, $processed_donation_ids, true ) ) {
2846 - $refunded_donations_total_amount += floatval( $refunded_donation->amount );
2847 - $refunded_donations_total_count++;
2848 - $processed_donation_ids[] = $donation_id;
2878 + $refund_day = gmdate( 'Y-m-d', strtotime( $refunded_donation->post_date ) );
2879 +
2880 + if ( ! isset( $refunds_by_date[ $refund_day ] ) ) {
2881 + $refunds_by_date[ $refund_day ] = array(
2882 + 'amount' => 0,
2883 + 'count' => 0,
2884 + );
2849 2885 }
2886 +
2887 + $refunds_by_date[ $refund_day ]['amount'] += floatval( $refunded_donation->amount );
2888 + ++$refunds_by_date[ $refund_day ]['count'];
2889 +
2890 + $refunded_donations_total_amount += floatval( $refunded_donation->amount );
2891 + ++$refunded_donations_total_count;
2850 2892 }
2851 2893 }
2852 2894
2853 - // Then, organize by day for the breakdown
2854 - foreach ( $days as $day ) {
2855 - $donation_day = gmdate( 'Y-m-d', strtotime( $day ) );
2856 - $refunded_donation_amount = 0;
2857 - $refunded_donation_count = 0;
2858 - $processed_day_donation_ids = array(); // Track unique donation IDs per day
2859 - if ( ! empty( $refunded_donations ) ) :
2860 - foreach ( $refunded_donations as $refunded_donation ) {
2861 - // if the refunded donation post date is the same day as the donation_day, add it to the refunded total.
2862 - $refund_date = gmdate( 'Y-m-d', strtotime( $refunded_donation->post_date_gmt ) );
2863 - if ( $refund_date === $donation_day ) {
2864 - $donation_id = isset( $refunded_donation->donation_id ) ? intval( $refunded_donation->donation_id ) : 0;
2865 - // Only count each unique donation ID once per day
2866 - if ( $donation_id > 0 && ! in_array( $donation_id, $processed_day_donation_ids, true ) ) {
2867 - $refunded_donation_amount += floatval( $refunded_donation->amount );
2868 - ++$refunded_donation_count;
2869 - $processed_day_donation_ids[] = $donation_id;
2870 - }
2871 - }
2872 - }
2873 - endif;
2874 - $refunds_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day ) );
2875 - $refunds_by_day[ $donation_day ]['amount'] = $refunded_donation_amount;
2876 - $refunds_by_day[ $donation_day ]['donors'] = $refunded_donation_count;
2895 + foreach ( $days as $day_unformatted => $day_label ) {
2896 + $donation_day = gmdate( 'Y-m-d', strtotime( $day_unformatted ) );
2897 +
2898 + $refunds_by_day[ $donation_day ]['label'] = gmdate( 'F d, Y', strtotime( $day_unformatted ) );
2899 + $refunds_by_day[ $donation_day ]['amount'] = isset( $refunds_by_date[ $donation_day ] ) ? $refunds_by_date[ $donation_day ]['amount'] : 0;
2900 + $refunds_by_day[ $donation_day ]['donors'] = isset( $refunds_by_date[ $donation_day ] ) ? $refunds_by_date[ $donation_day ]['count'] : 0;
2877 2901 }
2878 2902
2879 2903 return array(
2880 2904 'donations' => $refunded_donations,
@@ -3204,14 +3228,14 @@
3204 3228 <p><?php echo esc_html__( 'Donors', 'charitable' ); ?></p>
3205 3229 <p><?php echo esc_html( $to_date_range ); ?></p>
3206 3230 </div>
3207 3231 <div class="charitable-container charitable-report-ui charitable-card">
3208 - <strong><span id="charitable-top-donor-count"><?php echo charitable_format_money( $total_amount_donations_from, 2, true ); // phpcs:ignore ?></span></strong>
3232 + <strong><span id="charitable-top-donor-count"><?php echo charitable_format_money( $total_amount_donations_from, false, true ); // phpcs:ignore ?></span></strong>
3209 3233 <p><?php echo esc_html__( 'Donations', 'charitable' ); ?></p>
3210 3234 <p><?php echo esc_html( $from_date_range ); ?></p>
3211 3235 </div>
3212 3236 <div class="charitable-container charitable-report-ui charitable-card">
3213 - <strong><span id="charitable-top-donor-count"><?php echo charitable_format_money( $total_amount_donations_to, 2, true ); // phpcs:ignore ?></span></strong>
3237 + <strong><span id="charitable-top-donor-count"><?php echo charitable_format_money( $total_amount_donations_to, false, true ); // phpcs:ignore ?></span></strong>
3214 3238 <p><?php echo esc_html__( 'Donations', 'charitable' ); ?></p>
3215 3239 <p><?php echo esc_html( $to_date_range ); ?></p>
3216 3240 </div>
3217 3241 </div>
@@ -3447,9 +3471,9 @@
3447 3471 foreach ( $donors_data as $donor_id => $data ) :
3448 3472
3449 3473 $donor_name = $data->donor_first_name . ' ' . $data->donor_last_name;
3450 3474 $donor_email = $data->donor_email;
3451 - $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . '/images/misc/placeholder-avatar-small.jpg';
3475 + $donor_avatar = ( is_email( $donor_email ) ) ? get_avatar_url( $donor_email ) : charitable()->get_path( 'assets', false ) . 'images/misc/placeholder-avatar-small.jpg';
3452 3476 $total_donation_count_compare_to = $data->total_donation_count_compare_to;
3453 3477 $total_donation_amount_compare_to = number_format( $data->total_donation_amount_compare_to, 2 );
3454 3478 $total_donation_count_compare_from = $data->total_donation_count_compare_from;
3455 3479 $total_donation_amount_compare_from = number_format( $data->total_donation_amount_compare_from, 2 );
@@ -3464,22 +3488,22 @@
3464 3488 <tr id="type-lybunt-donor-<?php echo intval( $donor_id ); ?>" class="row-type-lybunt">
3465 3489 <td class="charitable-avatar" style="width: 32px;"><img src="<?php echo esc_url( $donor_avatar ); ?>" class="avatar avatar-<?php echo esc_html( $donor_id ); ?> photo" width="32" height="32" alt="<?php esc_html__( 'Profile picture of', 'charitable' ); ?> <?php echo esc_html( $donor_name ); ?>"></td>
3466 3490 <td class="donated column-date" data-colname="<?php echo esc_html__( 'Date.', 'charitable' ); ?>"><?php echo esc_html( $donor_name ); ?><br/><?php echo esc_html( $donor_email ); ?></td>
3467 3491 <td class="donated column-donations" data-colname="Donations"><?php echo charitable_format_money( $last_donation_amount ); // phpcs:ignore ?></td>
3468 - <td class="donated column-donors" data-colname="Number of Donors"><?php echo esc_html( $total_donation_count_compare_from ); ?> <?php echo esc_html__( 'Donations', 'charitable' ); ?><br/><small><?php echo charitable_format_money( $total_donation_amount_compare_from, 2, true ); // phpcs:ignore ?></small></td>
3469 - <td class="donated column-donors" data-colname="Number of Donors"><?php echo esc_html( $total_donation_count_compare_to ); ?> <?php echo esc_html__( 'Donations', 'charitable' ); ?><br/><small><?php echo charitable_format_money( $total_donation_amount_compare_to, 2, true ); // phpcs:ignore ?></small></td>
3492 + <td class="donated column-donors" data-colname="Number of Donors"><?php echo esc_html( $total_donation_count_compare_from ); ?> <?php echo esc_html__( 'Donations', 'charitable' ); ?><br/><small><?php echo charitable_format_money( $total_donation_amount_compare_from, false, true ); // phpcs:ignore ?></small></td>
3493 + <td class="donated column-donors" data-colname="Number of Donors"><?php echo esc_html( $total_donation_count_compare_to ); ?> <?php echo esc_html__( 'Donations', 'charitable' ); ?><br/><small><?php echo charitable_format_money( $total_donation_amount_compare_to, false, true ); // phpcs:ignore ?></small></td>
3470 3494 <td class="donated column-refunds" data-colname="Refunds"><?php echo $campaign_links; // phpcs:ignore ?></td>
3471 - <td class="donated column-net " data-colname="Net"><?php echo charitable_format_money( $total_life_amount, 2, true ); // phpcs:ignore ?></td>
3495 + <td class="donated column-net " data-colname="Net"><?php echo charitable_format_money( $total_life_amount, false, true ); // phpcs:ignore ?></td>
3472 3496 <?php
3473 3497
3474 3498 if ( $show_actions ) :
3475 3499 if ( $link && charitable_is_pro() ) :
3476 3500
3477 - $actions_display = '<td class="donated column-actions " data-colname="Actions"><a class="charitable-campaign-action-button" title="' . esc_html__( 'View User', 'charitable' ) . '" href="' . esc_url( $link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . '/images/icons/eye.svg" width="14" height="14" alt="' . esc_html__( 'View User', 'charitable' ) . '" /></td>';
3501 + $actions_display = '<td class="donated column-actions " data-colname="Actions"><a class="charitable-campaign-action-button" title="' . esc_html__( 'View User', 'charitable' ) . '" href="' . esc_url( $link ) . '" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . 'images/icons/eye.svg" width="14" height="14" alt="' . esc_html__( 'View User', 'charitable' ) . '" /></td>';
3478 3502
3479 3503 else :
3480 3504
3481 - $actions_display = '<td class="donated column-actions " data-colname="Actions"><a class="charitable-campaign-action-button" title="' . esc_html__( 'View User', 'charitable' ) . '" href="#" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . '/images/icons/eye.svg" width="14" height="14" alt="' . esc_html__( 'View User', 'charitable' ) . '" /></td>';
3505 + $actions_display = '<td class="donated column-actions " data-colname="Actions"><a class="charitable-campaign-action-button" title="' . esc_html__( 'View User', 'charitable' ) . '" href="#" target="_blank"><img src="' . charitable()->get_path( 'assets', false ) . 'images/icons/eye.svg" width="14" height="14" alt="' . esc_html__( 'View User', 'charitable' ) . '" /></td>';
3482 3506
3483 3507 endif;
3484 3508 endif;
3485 3509
@@ -3605,8 +3629,69 @@
3605 3629 return false;
3606 3630 }
3607 3631
3608 3632 /**
3633 + * Get the date of the earliest donation on the site.
3634 + *
3635 + * Used to build the "All Time" preset in the reporting date picker, so that
3636 + * lifetime figures are one click away rather than a hand-typed range.
3637 + *
3638 + * Every valid donation status is considered, not just completed ones, because
3639 + * the reports can be filtered by status and a narrower floor would hide the
3640 + * earliest refunded or cancelled donation.
3641 + *
3642 + * @since 1.8.12.4
3643 + *
3644 + * @return string Date in Y/m/d format. Falls back to one month ago when the
3645 + * site has no donations yet.
3646 + */
3647 + public function get_earliest_donation_date() {
3648 +
3649 + $cached = get_transient( 'charitable-reports-earliest-donation-date' );
3650 +
3651 + if ( false !== $cached ) {
3652 + return $cached;
3653 + }
3654 +
3655 + global $wpdb;
3656 +
3657 + $statuses = array_keys( charitable_get_valid_donation_statuses() );
3658 +
3659 + // `charitable_donation_statuses` is a public filter, and an empty list would
3660 + // build `IN ( )`. Nothing to measure in that case, so use the fallback.
3661 + if ( empty( $statuses ) ) {
3662 + return gmdate( 'Y/m/d', strtotime( '-1 month' ) );
3663 + }
3664 +
3665 + $placeholders = implode( ', ', array_fill( 0, count( $statuses ), '%s' ) );
3666 +
3667 + $earliest = $wpdb->get_var( // phpcs:ignore
3668 + $wpdb->prepare(
3669 + "SELECT MIN( post_date ) FROM {$wpdb->posts}
3670 + WHERE post_type = 'donation'
3671 + AND post_status IN ( {$placeholders} )", // phpcs:ignore
3672 + $statuses
3673 + )
3674 + );
3675 +
3676 + if ( empty( $earliest ) || '0000-00-00 00:00:00' === $earliest ) {
3677 + /*
3678 + * Deliberately not cached. A site with no donations today may be one
3679 + * import away from four years of history, and a cached fallback would
3680 + * leave "All Time" starting a month ago for a full day afterwards -
3681 + * which is the exact wrong-figure trap this preset exists to close.
3682 + */
3683 + return gmdate( 'Y/m/d', strtotime( '-1 month' ) );
3684 + }
3685 +
3686 + $date = gmdate( 'Y/m/d', strtotime( $earliest ) );
3687 +
3688 + set_transient( 'charitable-reports-earliest-donation-date', $date, DAY_IN_SECONDS );
3689 +
3690 + return $date;
3691 + }
3692 +
3693 + /**
3609 3694 * Get localized strings.
3610 3695 *
3611 3696 * @since 1.8.1
3612 3697 *
@@ -3665,9 +3750,18 @@
3665 3750 'payment_keys' => (array) array_reverse( $this->payment_keys ),
3666 3751 ),
3667 3752 'default_start_date' => current_time( gmdate( 'Y-m-d', strtotime( '-1 month' ) ) ),
3668 3753 'default_end_date' => current_time( gmdate( 'Y-m-d' ) ),
3754 + 'earliest_donation_date' => $this->get_earliest_donation_date(),
3755 + // The date picker uses each preset's label as its object key, so the
3756 + // label has to travel to JS rather than being written there. It rides
3757 + // this same array as `earliest_donation_date` on purpose: the
3758 + // reporting JS contains no wp.i18n calls, so a second localization
3759 + // mechanism for one string would be the inconsistent choice.
3760 + 'all_time_label' => __( 'All Time', 'charitable' ),
3669 3761 'advanced_reports' => $charitable_reports_advanced_report_strings,
3762 + 'i18n_report_timeout' => __( 'This report took too long to load and was stopped by the server. The figures have been cleared so they are not mistaken for the range you asked for. Try a shorter date range.', 'charitable' ),
3763 + 'i18n_report_error' => __( 'This report could not be loaded. The figures have been cleared so they are not mistaken for the range you asked for. Please try again.', 'charitable' ),
3670 3764 );
3671 3765
3672 3766 $strings = apply_filters( 'charitable_reporting_strings', $strings );
3673 3767