From 610992f626384ccca5dc16d4ca369006c32caaa5 Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 28 Apr 2026 16:26:46 +0200 Subject: [PATCH 01/11] Add KPI block styles --- assets/css/analytics.scss | 135 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/assets/css/analytics.scss b/assets/css/analytics.scss index 3a974fe..242cabe 100644 --- a/assets/css/analytics.scss +++ b/assets/css/analytics.scss @@ -311,6 +311,141 @@ $mq-xs: 600px; } } +// ----------------------------------------------------------------------------- +// Audience Overview KPI block +// ----------------------------------------------------------------------------- +.mailchimp-sf-ao { + + &__metrics { + display: grid; + gap: 24px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin: 0 0 16px; + } + + &__metric { + display: flex; + flex-direction: column; + min-width: 0; + } + + &__metric-label { + align-self: flex-start; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + color: var(--mc-sa-text-strong); + font-size: 14px; + font-weight: 600; + margin: 0; + + &::after { + background-image: linear-gradient( + 90deg, + color-mix(in srgb, var(--mailchimp-color-link, #017e89) 45%, #ffffff) 5px, + transparent 5px + ); + background-repeat: repeat-x; + background-size: 9px 2px; + content: ""; + display: block; + height: 2px; + margin-top: 6px; + } + } + + &__metric-value { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + color: var(--mc-sa-text-strong); + font-size: 32px; + font-weight: 700; + letter-spacing: -0.01em; + line-height: 1.1; + margin: 8px 0 0; + } + + &.is-loading &__metric-value, + &.is-error &__metric-value { + color: var(--mc-sa-grey); + opacity: 0.85; + } + + &__error-banner { + align-items: center; + background: var(--mc-sa-error-bg); + border: 1px solid var(--mc-sa-error-border); + border-radius: 8px; + display: flex; + gap: 12px; + margin: 4px 0 16px; + padding: 12px 14px; + + &[hidden] { + display: none; + } + } + + &__error-banner-icon { + align-items: center; + color: var(--mc-sa-error-text); + display: inline-flex; + flex-shrink: 0; + justify-content: center; + line-height: 0; + } + + &__error-banner-body { + flex: 1 1 auto; + min-width: 0; + } + + &__error-banner-title { + color: var(--mc-sa-error-title); + font-size: 13px; + font-weight: 600; + line-height: 1.3; + margin: 0 0 2px; + } + + &__error-banner-message { + color: var(--mc-sa-error-text); + font-size: 13px; + line-height: 1.4; + margin: 0; + } + + &__error-banner-action { + flex-shrink: 0; + } + + button#mailchimp-sf-ao-error-retry { + background-color: #fff; + border: 1px solid #ccd6dc; + + &:hover, + &:focus, + &:active { + background-color: #f6f7f7; + } + } +} + +@media screen and (max-width: $mq-medium) { + + .mailchimp-sf-ao__metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media screen and (max-width: $mq-xs) { + + .mailchimp-sf-ao__metrics { + grid-template-columns: 1fr; + } + + .mailchimp-sf-ao__metric-value { + font-size: 28px; + } +} + // ----------------------------------------------------------------------------- // Shared analytics chart-card // ----------------------------------------------------------------------------- From b8aaa46330cbaba3e8328ce44266c4c436e2781f Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 28 Apr 2026 16:27:41 +0200 Subject: [PATCH 02/11] Add Audience Overview --- assets/js/analytics.js | 208 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/assets/js/analytics.js b/assets/js/analytics.js index 85209e7..602a202 100644 --- a/assets/js/analytics.js +++ b/assets/js/analytics.js @@ -489,6 +489,8 @@ import { __ } from '@wordpress/i18n'; } /** + * Render the bar+line chart from API rows. + * * @param {Array} rows Payload `data` rows from the API. */ function renderChart(rows) { @@ -1160,6 +1162,212 @@ import { __ } from '@wordpress/i18n'; }); })(); + /** + * Audience Overview KPI block + */ + (function audienceOverviewModule() { + const section = document.querySelector('[data-section="audience-overview"]'); + if (!section) { + return; + } + + const subscribersEl = document.getElementById('mailchimp-sf-ao-total-subscribers'); + const viewsEl = document.getElementById('mailchimp-sf-ao-views'); + const submissionsEl = document.getElementById('mailchimp-sf-ao-submissions'); + const rateEl = document.getElementById('mailchimp-sf-ao-rate'); + const dateRangeEl = document.getElementById('mailchimp-sf-ao-daterange'); + const errorBannerEl = document.getElementById('mailchimp-sf-ao-error-banner'); + const errorMessageEl = document.getElementById('mailchimp-sf-ao-error-message'); + const retryBtnEl = document.getElementById('mailchimp-sf-ao-error-retry'); + + const STRINGS = { + loadingSubtitle: __('Loading audience overview…', 'mailchimp'), + errorDefault: __( + 'Unable to load audience overview. Please check your connection and try again.', + 'mailchimp', + ), + }; + + const STATE_CLASSES = ['is-loading', 'is-ready', 'is-error']; + + let inFlight = null; + let lastDetail = null; + + function setState(state) { + STATE_CLASSES.forEach(function (cls) { + section.classList.toggle(cls, cls === `is-${state}`); + }); + } + + function setSubtitle(text) { + if (dateRangeEl) { + dateRangeEl.textContent = text || ''; + } + } + + function setErrorBanner(visible, message) { + if (!errorBannerEl) { + return; + } + if (visible) { + if (errorMessageEl) { + errorMessageEl.textContent = message || STRINGS.errorDefault; + } + errorBannerEl.hidden = false; + } else { + errorBannerEl.hidden = true; + } + } + + function setPlaceholders() { + [subscribersEl, viewsEl, submissionsEl, rateEl].forEach(function (el) { + if (el) { + el.textContent = '-'; + } + }); + } + + function formatRangeLabel(from, to) { + try { + const fromDate = new Date(`${from}T00:00:00`); + const toDate = new Date(`${to}T00:00:00`); + const fmt = new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + return `${fmt.format(fromDate)} – ${fmt.format(toDate)}`; + } catch (err) { + return `${from} – ${to}`; + } + } + + function formatNumber(n) { + if (n === null || typeof n === 'undefined') { + return '-'; + } + try { + return new Intl.NumberFormat().format(n); + } catch (err) { + return String(n); + } + } + + function showLoading() { + setErrorBanner(false); + setSubtitle(STRINGS.loadingSubtitle); + setPlaceholders(); + setState('loading'); + } + + function showError(message) { + if (lastDetail && lastDetail.from && lastDetail.to) { + setSubtitle(formatRangeLabel(lastDetail.from, lastDetail.to)); + } + setPlaceholders(); + setErrorBanner(true, message); + setState('error'); + } + + function render(data, fromLabel, toLabel) { + setErrorBanner(false); + setSubtitle(formatRangeLabel(fromLabel, toLabel)); + + if (subscribersEl) { + subscribersEl.textContent = formatNumber(data.total_subscribers); + } + if (viewsEl) { + viewsEl.textContent = formatNumber(data.total_views); + } + if (submissionsEl) { + submissionsEl.textContent = formatNumber(data.total_submissions); + } + if (rateEl) { + const rate = data.total_conversion_rate; + rateEl.textContent = + rate === null || typeof rate === 'undefined' + ? '-' + : `${Number(rate).toFixed(2)}%`; + } + + setState('ready'); + } + + function fetchOverview(detail) { + if (!window.mailchimpSFAnalytics || !window.mailchimpSFAnalytics.ajax_url) { + showError(); + return; + } + if (!detail || !detail.listId || !detail.from || !detail.to) { + return; + } + + lastDetail = { + listId: detail.listId, + from: detail.from, + to: detail.to, + }; + + if (inFlight && typeof inFlight.abort === 'function') { + inFlight.abort(); + } + + const controller = + typeof window.AbortController !== 'undefined' ? new AbortController() : null; + inFlight = controller; + + const formData = new FormData(); + formData.append('action', 'mailchimp_sf_get_audience_overview'); + formData.append('nonce', window.mailchimpSFAnalytics.nonce); + formData.append('list_id', detail.listId); + formData.append('date_from', detail.from); + formData.append('date_to', detail.to); + + showLoading(); + + fetch(window.mailchimpSFAnalytics.ajax_url, { + method: 'POST', + body: formData, + credentials: 'same-origin', + signal: controller ? controller.signal : undefined, + }) + .then(function (response) { + return response.json().catch(function () { + return null; + }); + }) + .then(function (body) { + inFlight = null; + if (!body || body.success !== true || !body.data) { + const message = + body && body.data && body.data.message ? body.data.message : ''; + showError(message); + return; + } + render(body.data, detail.from, detail.to); + }) + .catch(function (err) { + if (err && err.name === 'AbortError') { + return; + } + inFlight = null; + showError(); + }); + } + + if (retryBtnEl) { + retryBtnEl.addEventListener('click', function () { + if (lastDetail) { + fetchOverview(lastDetail); + } + }); + } + + document.addEventListener('mailchimp-analytics-refresh', function (e) { + fetchOverview(e.detail); + }); + })(); + // Initialize. updateTriggerLabel(); syncDateInputs(); From 7c36008e68c2af08bd67f100dc4b08fa3c0c7f76 Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 28 Apr 2026 16:28:12 +0200 Subject: [PATCH 03/11] Add Audience Overview block --- includes/admin/templates/analytics.php | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/includes/admin/templates/analytics.php b/includes/admin/templates/analytics.php index b944399..825091e 100644 --- a/includes/admin/templates/analytics.php +++ b/includes/admin/templates/analytics.php @@ -100,6 +100,96 @@ +
+
+

+ +

+

+
+ + + +
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
Date: Tue, 28 Apr 2026 16:28:23 +0200 Subject: [PATCH 04/11] Initial commit --- .../class-mailchimp-audience-overview.php | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 includes/class-mailchimp-audience-overview.php diff --git a/includes/class-mailchimp-audience-overview.php b/includes/class-mailchimp-audience-overview.php new file mode 100644 index 0000000..a899c75 --- /dev/null +++ b/includes/class-mailchimp-audience-overview.php @@ -0,0 +1,119 @@ + esc_html__( 'Unauthorized.', 'mailchimp' ) ), 403 ); + } + + check_ajax_referer( 'mailchimp_sf_analytics_admin_nonce', 'nonce' ); + + $list_id = isset( $_POST['list_id'] ) ? sanitize_text_field( wp_unslash( $_POST['list_id'] ) ) : ''; + $date_from = isset( $_POST['date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['date_from'] ) ) : ''; + $date_to = isset( $_POST['date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['date_to'] ) ) : ''; + + if ( empty( $list_id ) ) { + wp_send_json_error( array( 'message' => esc_html__( 'Please select a list.', 'mailchimp' ) ), 400 ); + } + + if ( ! $this->is_valid_date( $date_from ) || ! $this->is_valid_date( $date_to ) ) { + wp_send_json_error( array( 'message' => esc_html__( 'Invalid date range.', 'mailchimp' ) ), 400 ); + } + + if ( strtotime( $date_from ) > strtotime( $date_to ) ) { + wp_send_json_error( array( 'message' => esc_html__( 'Start date must be before end date.', 'mailchimp' ) ), 400 ); + } + + $analytics_data = new Mailchimp_Analytics_Data(); + $totals = $analytics_data->get_totals( $list_id, $date_from, $date_to ); + $total_views = isset( $totals['total_views'] ) ? (int) $totals['total_views'] : 0; + $total_submissions = isset( $totals['total_submissions'] ) ? (int) $totals['total_submissions'] : 0; + + wp_send_json_success( + array( + 'total_subscribers' => $this->fetch_total_subscribers( $list_id ), + 'total_views' => $total_views, + 'total_submissions' => $total_submissions, + 'total_conversion_rate' => $this->conversion_rate( $total_submissions, $total_views ), + ) + ); + } + + /** + * Fetch (and cache) the current total subscriber count for a list. + * + * @param string $list_id List ID. + * @return int|null + */ + public function fetch_total_subscribers( string $list_id ): ?int { + $cache_key = self::SUBSCRIBERS_CACHE_PREFIX . md5( $list_id ); + $cached = get_transient( $cache_key ); + + if ( false !== $cached ) { + return (int) $cached; + } + + $api = mailchimp_sf_get_api(); + if ( ! $api ) { + return null; + } + + $response = $api->get( + 'lists/' . rawurlencode( $list_id ), + 1, + array( 'stats.member_count' ) + ); + + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + return null; + } + + if ( ! isset( $response['stats']['member_count'] ) ) { + return null; + } + + $count = (int) $response['stats']['member_count']; + set_transient( $cache_key, $count, self::SUBSCRIBERS_CACHE_TTL ); + + return $count; + } +} From 1b7d1a870bac457a2c80c951158e4b0281467119 Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 28 Apr 2026 16:29:18 +0200 Subject: [PATCH 05/11] Move conversion rate to trait --- includes/class-mailchimp-form-performance.php | 15 --------------- includes/trait-mailchimp-analytics-bucketing.php | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/includes/class-mailchimp-form-performance.php b/includes/class-mailchimp-form-performance.php index 9216ad9..6e2e882 100644 --- a/includes/class-mailchimp-form-performance.php +++ b/includes/class-mailchimp-form-performance.php @@ -175,19 +175,4 @@ public function aggregate( array $rows, string $date_from, string $date_to ): ar ); } - /** - * Submissions ÷ views, as a percentage (0–100, two decimals). - * - * @param int $submissions Submission count. - * @param int $views View count. - * @return float - */ - private function conversion_rate( int $submissions, int $views ): float { - if ( $views <= 0 ) { - return 0.0; - } - $rate = ( $submissions / $views ) * 100; - return round( min( 100.0, $rate ), 2 ); - } - } diff --git a/includes/trait-mailchimp-analytics-bucketing.php b/includes/trait-mailchimp-analytics-bucketing.php index 613fe03..ddaad89 100644 --- a/includes/trait-mailchimp-analytics-bucketing.php +++ b/includes/trait-mailchimp-analytics-bucketing.php @@ -97,6 +97,21 @@ public function get_bucket_label( string $date, string $interval, $tz = null ): } } + /** + * Submissions ÷ views + * + * @param int $submissions Submission count. + * @param int $views View count. + * @return float + */ + protected function conversion_rate( int $submissions, int $views ): float { + if ( $views <= 0 ) { + return 0.0; + } + $rate = ( $submissions / $views ) * 100; + return round( min( 100.0, $rate ), 2 ); + } + /** * Validate a `Y-m-d` date string. * From ddadf0c416a65686c444d8bedd6ba6f5b04ce190 Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 28 Apr 2026 16:29:58 +0200 Subject: [PATCH 06/11] Load Mailchimp_Audience_Overview class --- mailchimp.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mailchimp.php b/mailchimp.php index 0ae5684..123ba41 100644 --- a/mailchimp.php +++ b/mailchimp.php @@ -133,6 +133,11 @@ function () { $form_performance = new Mailchimp_Form_Performance(); $form_performance->init(); +// Audience overview KPI block data class. +require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-audience-overview.php'; +$audience_overview = new Mailchimp_Audience_Overview(); +$audience_overview->init(); + // Deprecated functions. require_once plugin_dir_path( __FILE__ ) . 'includes/mailchimp-deprecated-functions.php'; From 4f69f42f0b057e9535d1189427f9e48fc15b3321 Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 5 May 2026 16:57:34 +0200 Subject: [PATCH 07/11] Add events --- assets/js/analytics.js | 110 ++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 63 deletions(-) diff --git a/assets/js/analytics.js b/assets/js/analytics.js index 602a202..eb3dc9c 100644 --- a/assets/js/analytics.js +++ b/assets/js/analytics.js @@ -663,9 +663,24 @@ import { __ } from '@wordpress/i18n'; renderChart(rows); } + /** + * Custom event so other analytics modules (Audience + * Overview, etc.) can render from the same fetch without making + * their own AJAX call. + * + * @param {string} name Event suffix — appended to `mailchimp-analytics-`. + * @param {object} eventDetail Payload passed as the event's `detail`. + */ + function broadcast(name, eventDetail) { + document.dispatchEvent( + new CustomEvent(`mailchimp-analytics-${name}`, { detail: eventDetail }), + ); + } + function fetchPerformance(detail) { if (!window.mailchimpSFAnalytics || !window.mailchimpSFAnalytics.ajax_url) { showError(); + broadcast('error', { message: STRINGS.errorDefault }); return; } if (!detail || !detail.listId || !detail.from || !detail.to) { @@ -695,6 +710,7 @@ import { __ } from '@wordpress/i18n'; formData.append('date_to', detail.to); showLoading(); + broadcast('loading', { from: detail.from, to: detail.to }); fetch(window.mailchimpSFAnalytics.ajax_url, { method: 'POST', @@ -713,9 +729,15 @@ import { __ } from '@wordpress/i18n'; const message = body && body.data && body.data.message ? body.data.message : ''; showError(message); + broadcast('error', { message: message || STRINGS.errorDefault }); return; } render(body.data, detail.from, detail.to); + broadcast('loaded', { + data: body.data, + from: detail.from, + to: detail.to, + }); }) .catch(function (err) { if (err && err.name === 'AbortError') { @@ -723,6 +745,7 @@ import { __ } from '@wordpress/i18n'; } inFlight = null; showError(); + broadcast('error', { message: STRINGS.errorDefault }); }); } @@ -1190,7 +1213,6 @@ import { __ } from '@wordpress/i18n'; const STATE_CLASSES = ['is-loading', 'is-ready', 'is-error']; - let inFlight = null; let lastDetail = null; function setState(state) { @@ -1293,79 +1315,41 @@ import { __ } from '@wordpress/i18n'; setState('ready'); } - function fetchOverview(detail) { - if (!window.mailchimpSFAnalytics || !window.mailchimpSFAnalytics.ajax_url) { - showError(); - return; - } - if (!detail || !detail.listId || !detail.from || !detail.to) { - return; + document.addEventListener('mailchimp-analytics-refresh', function (e) { + if (e.detail) { + lastDetail = { + listId: e.detail.listId, + from: e.detail.from, + to: e.detail.to, + }; } + }); - lastDetail = { - listId: detail.listId, - from: detail.from, - to: detail.to, - }; + document.addEventListener('mailchimp-analytics-loading', function () { + showLoading(); + }); - if (inFlight && typeof inFlight.abort === 'function') { - inFlight.abort(); + document.addEventListener('mailchimp-analytics-loaded', function (e) { + if (e.detail && e.detail.data) { + render(e.detail.data, e.detail.from, e.detail.to); } + }); - const controller = - typeof window.AbortController !== 'undefined' ? new AbortController() : null; - inFlight = controller; - - const formData = new FormData(); - formData.append('action', 'mailchimp_sf_get_audience_overview'); - formData.append('nonce', window.mailchimpSFAnalytics.nonce); - formData.append('list_id', detail.listId); - formData.append('date_from', detail.from); - formData.append('date_to', detail.to); - - showLoading(); - - fetch(window.mailchimpSFAnalytics.ajax_url, { - method: 'POST', - body: formData, - credentials: 'same-origin', - signal: controller ? controller.signal : undefined, - }) - .then(function (response) { - return response.json().catch(function () { - return null; - }); - }) - .then(function (body) { - inFlight = null; - if (!body || body.success !== true || !body.data) { - const message = - body && body.data && body.data.message ? body.data.message : ''; - showError(message); - return; - } - render(body.data, detail.from, detail.to); - }) - .catch(function (err) { - if (err && err.name === 'AbortError') { - return; - } - inFlight = null; - showError(); - }); - } + document.addEventListener('mailchimp-analytics-error', function (e) { + showError(e.detail && e.detail.message); + }); if (retryBtnEl) { retryBtnEl.addEventListener('click', function () { - if (lastDetail) { - fetchOverview(lastDetail); + if (!lastDetail) { + return; } + + document.dispatchEvent( + new CustomEvent('mailchimp-analytics-refresh', { detail: lastDetail }), + ); }); } - - document.addEventListener('mailchimp-analytics-refresh', function (e) { - fetchOverview(e.detail); - }); })(); // Initialize. From e637d1b2bbe03fd166b6e871de2fc42a7f326e18 Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 5 May 2026 16:58:05 +0200 Subject: [PATCH 08/11] Fetch subscribers --- includes/class-mailchimp-form-performance.php | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/includes/class-mailchimp-form-performance.php b/includes/class-mailchimp-form-performance.php index 6e2e882..099573c 100644 --- a/includes/class-mailchimp-form-performance.php +++ b/includes/class-mailchimp-form-performance.php @@ -17,6 +17,16 @@ class Mailchimp_Form_Performance { use Mailchimp_Analytics_Bucketing; + /** + * Transient key prefix for the cached total subscriber count. + */ + const SUBSCRIBERS_CACHE_PREFIX = 'mailchimp_sf_total_subscribers_'; + + /** + * Transient TTL for the cached total subscriber count. + */ + const SUBSCRIBERS_CACHE_TTL = 15 * MINUTE_IN_SECONDS; + /** * Register hooks. * @@ -62,9 +72,50 @@ public function handle_get() { $response = $this->aggregate( $rows, $date_from, $date_to ); + $response['total_subscribers'] = $this->fetch_total_subscribers( $list_id ); + wp_send_json_success( $response ); } + /** + * Fetch (and cache) the current total subscriber count for a list. + * + * @param string $list_id List ID. + * @return int|null + */ + public function fetch_total_subscribers( string $list_id ): ?int { + $cache_key = self::SUBSCRIBERS_CACHE_PREFIX . md5( $list_id ); + $cached = get_transient( $cache_key ); + + if ( false !== $cached ) { + return (int) $cached; + } + + $api = mailchimp_sf_get_api(); + if ( ! $api ) { + return null; + } + + $response = $api->get( + 'lists/' . rawurlencode( $list_id ), + 1, + array( 'stats.member_count' ) + ); + + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + return null; + } + + if ( ! isset( $response['stats']['member_count'] ) ) { + return null; + } + + $count = (int) $response['stats']['member_count']; + set_transient( $cache_key, $count, self::SUBSCRIBERS_CACHE_TTL ); + + return $count; + } + /** * Fetch daily totals from the analytics table for the selected list/range. * From b3ec25427109cf72744a8c2cdee601c45bf7edd8 Mon Sep 17 00:00:00 2001 From: alaca Date: Tue, 5 May 2026 16:59:15 +0200 Subject: [PATCH 09/11] Cleanup --- .../class-mailchimp-audience-overview.php | 119 ------------------ mailchimp.php | 6 - 2 files changed, 125 deletions(-) delete mode 100644 includes/class-mailchimp-audience-overview.php diff --git a/includes/class-mailchimp-audience-overview.php b/includes/class-mailchimp-audience-overview.php deleted file mode 100644 index a899c75..0000000 --- a/includes/class-mailchimp-audience-overview.php +++ /dev/null @@ -1,119 +0,0 @@ - esc_html__( 'Unauthorized.', 'mailchimp' ) ), 403 ); - } - - check_ajax_referer( 'mailchimp_sf_analytics_admin_nonce', 'nonce' ); - - $list_id = isset( $_POST['list_id'] ) ? sanitize_text_field( wp_unslash( $_POST['list_id'] ) ) : ''; - $date_from = isset( $_POST['date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['date_from'] ) ) : ''; - $date_to = isset( $_POST['date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['date_to'] ) ) : ''; - - if ( empty( $list_id ) ) { - wp_send_json_error( array( 'message' => esc_html__( 'Please select a list.', 'mailchimp' ) ), 400 ); - } - - if ( ! $this->is_valid_date( $date_from ) || ! $this->is_valid_date( $date_to ) ) { - wp_send_json_error( array( 'message' => esc_html__( 'Invalid date range.', 'mailchimp' ) ), 400 ); - } - - if ( strtotime( $date_from ) > strtotime( $date_to ) ) { - wp_send_json_error( array( 'message' => esc_html__( 'Start date must be before end date.', 'mailchimp' ) ), 400 ); - } - - $analytics_data = new Mailchimp_Analytics_Data(); - $totals = $analytics_data->get_totals( $list_id, $date_from, $date_to ); - $total_views = isset( $totals['total_views'] ) ? (int) $totals['total_views'] : 0; - $total_submissions = isset( $totals['total_submissions'] ) ? (int) $totals['total_submissions'] : 0; - - wp_send_json_success( - array( - 'total_subscribers' => $this->fetch_total_subscribers( $list_id ), - 'total_views' => $total_views, - 'total_submissions' => $total_submissions, - 'total_conversion_rate' => $this->conversion_rate( $total_submissions, $total_views ), - ) - ); - } - - /** - * Fetch (and cache) the current total subscriber count for a list. - * - * @param string $list_id List ID. - * @return int|null - */ - public function fetch_total_subscribers( string $list_id ): ?int { - $cache_key = self::SUBSCRIBERS_CACHE_PREFIX . md5( $list_id ); - $cached = get_transient( $cache_key ); - - if ( false !== $cached ) { - return (int) $cached; - } - - $api = mailchimp_sf_get_api(); - if ( ! $api ) { - return null; - } - - $response = $api->get( - 'lists/' . rawurlencode( $list_id ), - 1, - array( 'stats.member_count' ) - ); - - if ( is_wp_error( $response ) || ! is_array( $response ) ) { - return null; - } - - if ( ! isset( $response['stats']['member_count'] ) ) { - return null; - } - - $count = (int) $response['stats']['member_count']; - set_transient( $cache_key, $count, self::SUBSCRIBERS_CACHE_TTL ); - - return $count; - } -} diff --git a/mailchimp.php b/mailchimp.php index 123ba41..db2fa1e 100644 --- a/mailchimp.php +++ b/mailchimp.php @@ -133,12 +133,6 @@ function () { $form_performance = new Mailchimp_Form_Performance(); $form_performance->init(); -// Audience overview KPI block data class. -require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-audience-overview.php'; -$audience_overview = new Mailchimp_Audience_Overview(); -$audience_overview->init(); - - // Deprecated functions. require_once plugin_dir_path( __FILE__ ) . 'includes/mailchimp-deprecated-functions.php'; From a7276d2a850a6927444872a1250cbf1f164869fd Mon Sep 17 00:00:00 2001 From: alaca Date: Wed, 6 May 2026 10:08:06 +0200 Subject: [PATCH 10/11] chore: update comments --- assets/js/analytics.js | 2 +- includes/trait-mailchimp-analytics-bucketing.php | 6 ++++-- tests/cypress/support/functions/formPerformanceAjax.js | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/assets/js/analytics.js b/assets/js/analytics.js index eb3dc9c..0958260 100644 --- a/assets/js/analytics.js +++ b/assets/js/analytics.js @@ -1186,7 +1186,7 @@ import { __ } from '@wordpress/i18n'; })(); /** - * Audience Overview KPI block + * Audience Overview KPI block — Total subscribers, Form views, New submissions, Conversion rate. */ (function audienceOverviewModule() { const section = document.querySelector('[data-section="audience-overview"]'); diff --git a/includes/trait-mailchimp-analytics-bucketing.php b/includes/trait-mailchimp-analytics-bucketing.php index ddaad89..b3815d9 100644 --- a/includes/trait-mailchimp-analytics-bucketing.php +++ b/includes/trait-mailchimp-analytics-bucketing.php @@ -98,11 +98,13 @@ public function get_bucket_label( string $date, string $interval, $tz = null ): } /** - * Submissions ÷ views + * Submissions ÷ views, expressed as a percentage (0–100, two decimals). + * + * Returns 0 when there were no views * * @param int $submissions Submission count. * @param int $views View count. - * @return float + * @return float Conversion rate percentage in the range [0, 100]. */ protected function conversion_rate( int $submissions, int $views ): float { if ( $views <= 0 ) { diff --git a/tests/cypress/support/functions/formPerformanceAjax.js b/tests/cypress/support/functions/formPerformanceAjax.js index 27af12c..8bcec85 100644 --- a/tests/cypress/support/functions/formPerformanceAjax.js +++ b/tests/cypress/support/functions/formPerformanceAjax.js @@ -40,6 +40,10 @@ function buildSuccessData(overrides = {}) { total_views: 120, total_submissions: 24, total_conversion_rate: 20.0, + // `total_subscribers` is sourced from the Mailchimp List API and + // included in the same response so the Audience Overview KPI card + // can render from a single fetch shared with Form Performance. + total_subscribers: 5082, ...overrides, }; } From 03a5ec224d65555b7db54a394de8fa38e99fb26e Mon Sep 17 00:00:00 2001 From: alaca Date: Wed, 6 May 2026 10:08:43 +0200 Subject: [PATCH 11/11] Add Audience Overview tests --- tests/cypress/e2e/settings/analytics.test.js | 110 +++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/cypress/e2e/settings/analytics.test.js b/tests/cypress/e2e/settings/analytics.test.js index 83c5460..75afd50 100644 --- a/tests/cypress/e2e/settings/analytics.test.js +++ b/tests/cypress/e2e/settings/analytics.test.js @@ -446,6 +446,116 @@ describe('Analytics admin page', () => { cy.get('#mailchimp-sf-date-picker-label').should('have.text', 'Last 7 days'); }); }); + + describe('Audience Overview KPI block', () => { + const analyticsUrl = '/wp-admin/admin.php?page=mailchimp_sf_analytics'; + + function stubFormPerformance(replyFn) { + cy.intercept('POST', '**/admin-ajax.php', (req) => { + if (!isFormPerformanceRequest(req)) { + req.continue(); + return; + } + const payload = typeof replyFn === 'function' ? replyFn(req) : replyFn; + req.reply({ + statusCode: 200, + headers: { 'content-type': 'application/json; charset=UTF-8' }, + body: payload, + }); + }).as('formPerformance'); + } + + it('Audience Overview section shell (title, four KPIs)', () => { + stubFormPerformance(() => wpJsonSuccessFp(buildFormPerformanceData())); + cy.visit(analyticsUrl); + cy.wait('@formPerformance'); + cy.get('[data-section="audience-overview"]').should('be.visible'); + cy.get('#mailchimp-sf-ao-title').contains('Audience Overview'); + cy.get('#mailchimp-sf-ao-total-subscribers').should('exist'); + cy.get('#mailchimp-sf-ao-views').should('exist'); + cy.get('#mailchimp-sf-ao-submissions').should('exist'); + cy.get('#mailchimp-sf-ao-rate').should('exist'); + }); + + it('Success payload renders all four KPIs from the shared response', () => { + stubFormPerformance(() => + wpJsonSuccessFp( + buildFormPerformanceData({ + total_subscribers: 5082, + total_views: 3138, + total_submissions: 938, + total_conversion_rate: 29.89, + }), + ), + ); + cy.visit(analyticsUrl); + cy.wait('@formPerformance'); + cy.get('[data-section="audience-overview"]').should('have.class', 'is-ready'); + cy.get('#mailchimp-sf-ao-total-subscribers').should('contain', '5,082'); + cy.get('#mailchimp-sf-ao-views').should('contain', '3,138'); + cy.get('#mailchimp-sf-ao-submissions').should('contain', '938'); + cy.get('#mailchimp-sf-ao-rate').should('contain', '29.89%'); + }); + + it('Missing total_subscribers (API failure) renders an em dash', () => { + stubFormPerformance(() => + wpJsonSuccessFp( + buildFormPerformanceData({ + total_subscribers: null, + total_views: 100, + total_submissions: 25, + total_conversion_rate: 25.0, + }), + ), + ); + cy.visit(analyticsUrl); + cy.wait('@formPerformance'); + cy.get('#mailchimp-sf-ao-total-subscribers').should('contain', '-'); + cy.get('#mailchimp-sf-ao-views').should('contain', '100'); + }); + + it('API error shows error banner on the Audience Overview card', () => { + stubFormPerformance(() => wpJsonErrorFp('Audience overview stub failure')); + cy.visit(analyticsUrl); + cy.wait('@formPerformance'); + cy.get('[data-section="audience-overview"]').should('have.class', 'is-error'); + cy.get('#mailchimp-sf-ao-error-banner').should('be.visible'); + cy.get('#mailchimp-sf-ao-error-message').contains('Audience overview stub failure'); + }); + + it('Retry on Audience Overview triggers another shared request', () => { + let n = 0; + stubFormPerformance(() => { + n += 1; + if (n === 1) { + return wpJsonErrorFp('First request fails'); + } + return wpJsonSuccessFp(buildFormPerformanceData()); + }); + cy.visit(analyticsUrl); + cy.wait('@formPerformance'); + cy.get('[data-section="audience-overview"]').should('have.class', 'is-error'); + cy.get('#mailchimp-sf-ao-error-retry').click(); + cy.wait('@formPerformance'); + cy.get('[data-section="audience-overview"]').should('have.class', 'is-ready'); + cy.get('#mailchimp-sf-ao-error-banner').should('have.attr', 'hidden'); + }); + + it('Audience Overview does not fire its own AJAX action', () => { + stubFormPerformance(() => wpJsonSuccessFp(buildFormPerformanceData())); + cy.intercept('POST', '**/admin-ajax.php', (req) => { + if ( + typeof req.body === 'string' && + req.body.includes('mailchimp_sf_get_audience_overview') + ) { + throw new Error('Audience Overview should not fire its own AJAX call'); + } + }); + cy.visit(analyticsUrl); + cy.wait('@formPerformance'); + cy.get('[data-section="audience-overview"]').should('have.class', 'is-ready'); + }); + }); }); describe('When not connected', () => {