Dashboard — the product's landing/command-center screen. * * Deliberately built from ONLY data that already exists elsewhere in the * product (Form::/Entry::/Form_Start::/Smtp_Connection::/Email_Log:: — the * exact same classes Forms, Entries, Reports and Settings already read * from), never a new metric invented for this screen. Every number here is * something a user could already find by clicking into another screen; * this page's only job is answering "what's happening right now" without * making them do that clicking. * * Redesign (user feedback: "simple... not showing everything in a clean * and professional way... user gets benefit from it"). Two real gaps * closed rather than just re-skinned: * * 1. The Key Metrics row only ever showed raw counts with no sense of * direction — a "12" tells you nothing about whether that's better or * worse than last week. Reports_Page already solved exactly this * (Entry::percent_change(), Form_Start::percent_change(), a real * change-badge component) for its own Key Metrics; this screen adopts * the identical pattern and the identical CSS classes * (.chargeforms-stat-tile--metric.is-*, .chargeforms-stat-change) so * it reads as the same product, not a second, older-looking design. * 2. Nothing on the old Dashboard told a user what to actually DO — every * card was a passive report. "Needs Attention" is new: it surfaces * only the conditions that are true right now (unread backlog, failed * sends, no SMTP, a form that's never received a submission) with a * direct link to fix each one, and says so plainly when there's * nothing to act on rather than always rendering an empty shell. * * Not the literal click target of the top-level "ChargeForms" WP-admin * menu item — that's still Forms_List_Page (registered on the shared * 'chargeforms' slug, a WP-admin convention changing it would touch * every existing bookmark/redirect that expects page=chargeforms to be * the forms list). This screen is reachable from the in-page top nav * (Page_Header) and its own sidebar entry instead, positioned first. */ class Dashboard_Page { const CAPABILITY = 'chargeforms_manage_forms'; public function __construct() { add_action( 'admin_menu', [ $this, 'register' ], 8 ); } public function register() { add_submenu_page( 'chargeforms', __( 'Dashboard', 'chargeforms' ), __( 'Dashboard', 'chargeforms' ), self::CAPABILITY, 'chargeforms-dashboard', [ $this, 'render' ] ); } public function render() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'chargeforms' ) ); } $form_count = Form::count(); ?>

render_first_run(); ?> render_populated( $form_count ); ?>

resolve_range(); list( $prior_from, $prior_to ) = $this->prior_period( $date_from, $date_to ); $trend = Entry::counts_by_day( $range ); $submissions = array_sum( $trend ); $recent = Entry::recent( 5 ); $unread = Entry::count_unread(); $smtp_connected = Smtp_Connection::count() > 0; $failed_emails = Email_Log::query( [ 'status' => 'failed', 'per_page' => 1 ] )['total']; $all_forms = Form::all(); $forms_by_id = []; foreach ( $all_forms as $form ) { $forms_by_id[ $form['id'] ] = $form['title']; } /* * Key Metrics — the same four numbers, the same change-badge math, * and the same CSS classes Reports_Page's own Key Metrics section * uses (Entry::percent_change(), Form_Start's view/conversion * tracking) rather than a second, invented metric set. Genuinely * new information on THIS screen, not duplicated busywork: a user * previously had no reason to click into Reports just to learn * whether this week is trending up or down. */ $submissions_change = Entry::percent_change( 0, $date_from, $date_to ); $views = Form_Start::count_total( 0, $date_from, $date_to, 'view' ); $views_change = Form_Start::percent_change( 0, $date_from, $date_to, 'view' ); $conversion = $views > 0 ? round( ( $submissions / $views ) * 100, 1 ) : null; $prior_submissions = Entry::query( [ 'date_from' => $prior_from, 'date_to' => $prior_to, 'per_page' => 1 ] )['total']; $prior_views = Form_Start::count_total( 0, $prior_from, $prior_to, 'view' ); $prior_conversion = $prior_views > 0 ? ( $prior_submissions / $prior_views ) * 100 : null; $conversion_change = ( null !== $conversion && null !== $prior_conversion && $prior_conversion > 0 ) ? round( ( ( $conversion - $prior_conversion ) / $prior_conversion ) * 100 ) : null; $revenue = Entry::payment_total( $date_from, $date_to ); $prior_revenue = Entry::payment_total( $prior_from, $prior_to ); $revenue_change = $prior_revenue > 0 ? round( ( ( $revenue - $prior_revenue ) / $prior_revenue ) * 100 ) : null; // Two bulk lookups (not one per row) so flagging a "payment received" // row in the activity feed below never turns into N+1 queries — the // same idiom Entry::get_meta_bulk()'s own docblock prescribes. $recent_ids = array_column( $recent, 'id' ); $payment_txn_ids = $recent_ids ? Entry::get_meta_bulk( $recent_ids, 'payment_transaction_id' ) : []; $legacy_pi_ids = $recent_ids ? Entry::get_meta_bulk( $recent_ids, 'stripe_payment_intent_id' ) : []; ?>

render_change_badge( $submissions_change ); ?>
render_change_badge( $views_change ); ?>
render_change_badge( $conversion_change ); ?>
render_change_badge( $revenue_change ); ?>

render_trend_chart( $trend ); ?>
render_needs_attention( $unread, $failed_emails, $smtp_connected, $all_forms ); ?>

render_top_forms( $all_forms ); ?>

0 ) { $items[] = [ 'icon' => 'mail', 'tone' => 'is-warn', 'text' => sprintf( /* translators: %d: number of unread entries */ _n( '%d unread submission', '%d unread submissions', $unread, 'chargeforms' ), $unread ), 'url' => admin_url( 'admin.php?page=chargeforms-entries&status=unread' ), 'cta' => __( 'Review', 'chargeforms' ), ]; } if ( ! $smtp_connected ) { $items[] = [ 'icon' => 'alert-triangle', 'tone' => 'is-warn', 'text' => __( 'No SMTP connection — notification emails may land in spam or fail silently', 'chargeforms' ), 'url' => admin_url( 'admin.php?page=chargeforms-settings§ion=email-delivery' ), 'cta' => __( 'Connect', 'chargeforms' ), ]; } if ( $failed_emails > 0 ) { $items[] = [ 'icon' => 'alert-triangle', 'tone' => 'is-error', 'text' => sprintf( /* translators: %d: number of failed emails */ _n( '%d email failed to send', '%d emails failed to send', $failed_emails, 'chargeforms' ), $failed_emails ), 'url' => admin_url( 'admin.php?page=chargeforms-settings§ion=email-delivery' ), 'cta' => __( 'Investigate', 'chargeforms' ), ]; } if ( count( $all_forms ) > 0 ) { $form_ids = array_column( $all_forms, 'id' ); $all_time_counts = Entry::count_by_form( $form_ids ); $untested = array_values( array_filter( $all_forms, function ( $form ) use ( $all_time_counts ) { return empty( $all_time_counts[ $form['id'] ] ); } ) ); if ( ! empty( $untested ) ) { $names = array_slice( array_column( $untested, 'title' ), 0, 2 ); $text = 1 === count( $untested ) ? sprintf( /* translators: %s: form title */ __( '"%s" has never received a submission', 'chargeforms' ), $names[0] ) : sprintf( /* translators: %d: number of forms with zero submissions */ _n( '%d form has never received a submission', '%d forms have never received a submission', count( $untested ), 'chargeforms' ), count( $untested ) ); $items[] = [ 'icon' => 'info', 'tone' => 'is-neutral', 'text' => $text, 'url' => admin_url( 'admin.php?page=chargeforms-entries&form_id=' . (int) $untested[0]['id'] ), 'cta' => __( 'View', 'chargeforms' ), ]; } } ?>

$v ) { $points[] = [ $left + $i * $step, $baseline - ( $v / $max ) * ( $baseline - $top ), ]; } $path = sprintf( 'M%.1F,%.1F', $points[0][0], $points[0][1] ); for ( $i = 0; $i < $count - 1; $i++ ) { [ $x0, $y0 ] = $points[ $i ]; [ $x1, $y1 ] = $points[ $i + 1 ]; $dx = ( $x1 - $x0 ) / 3; $path .= sprintf( ' C%.1F,%.1F %.1F,%.1F %.1F,%.1F', $x0 + $dx, $y0, $x1 - $dx, $y1, $x1, $y1 ); } $area_path = $path . sprintf( ' L%.1F,%.1F L%.1F,%.1F Z', $right, $baseline, $left, $baseline ); $last_idx = $count - 1; ?> [ $x, $y ] ) : ?> 7 ? 'M j' : 'D'; ?> $day ) : ?> · ' . esc_html__( 'No prior period', 'chargeforms' ) . ''; return; } $is_up = $change >= 0; printf( '%s %s%%', esc_attr( $is_up ? 'is-up' : 'is-down' ), wp_kses( Icon::html( $is_up ? 'trending-up' : 'trending-down', 12 ), Icon::allowed_svg_html() ), esc_html( ( $is_up ? '+' : '' ) . $change ) ); } /** * Upgraded from the old donut-only "Forms by Submissions" (top 3, * no links, no counts beyond a percentage) to the same ranked * horizontal-bar list Reports_Page's "Top Performing Forms" already * uses — same CSS classes, so this gets that component's exact * polish (hover, staggered entrance animation, tooltips) for free. * All-time counts, not range-scoped: matches this card's original * intent ("which forms matter most, overall"), distinct from the * period-scoped Key Metrics above it. */ private function render_top_forms( array $all_forms ) { if ( empty( $all_forms ) ) { return; } $form_ids = array_column( $all_forms, 'id' ); $counts = Entry::count_by_form( $form_ids ); $total = array_sum( $counts ); ?>

$count ) : ?> form_title( $all_forms, $form_id ); $tooltip = sprintf( /* translators: 1: form title, 2: submission count, 3: percentage of all submissions */ __( '%1$s — %2$s submissions (%3$d%% of total)', 'chargeforms' ), $title, number_format_i18n( $count ), round( ( $count / max( 1, $total ) ) * 100 ) ); ?>