'ordersource', 'source' => 'orderutmsource', 'medium' => 'orderutmmedium', 'campaign' => 'orderutmcampaign', 'term' => 'orderutmterm', 'content' => 'orderutmcontent', ); const ORDER = array( 'channel', 'source', 'medium', 'campaign', 'term', 'content' ); private static function labels() { return array( 'channel' => __( 'Channel', 'ai-eshop-optimizer' ), 'source' => __( 'Source', 'ai-eshop-optimizer' ), 'medium' => _x( 'Medium', 'UTM parameter', 'ai-eshop-optimizer' ), 'campaign' => __( 'Campaign', 'ai-eshop-optimizer' ), 'term' => __( 'Term', 'ai-eshop-optimizer' ), 'content' => __( 'Content', 'ai-eshop-optimizer' ), ); } public function get_id() { return 'utm_explorer'; } public function get_icon() { return 'hash'; } public function get_label() { return __( 'Drill-down by campaign tag', 'ai-eshop-optimizer' ); } public function get_group() { return 'attribution'; } public function get_section() { return 'sources'; } public function get_priority() { return 16; } public function register_hooks() { add_action( 'wp_ajax_aieo_utm_explore', array( $this, 'ajax_explore' ) ); add_action( 'wp_ajax_aieo_utm_search', array( $this, 'ajax_search' ) ); } private function guard() { check_ajax_referer( self::NONCE, 'nonce' ); if ( ! current_user_can( self::CAP ) ) { wp_send_json_error( array( 'msg' => 'forbidden' ), 403 ); } } private function dates() { $to = isset( $_REQUEST['to'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['to'] ) ) : ''; $from = isset( $_REQUEST['from'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['from'] ) ) : ''; $d = '/^\d{4}-\d{2}-\d{2}$/'; if ( ! preg_match( $d, $to ) ) { $to = gmdate( 'Y-m-d' ); } if ( ! preg_match( $d, $from ) ) { $from = gmdate( 'Y-m-d', strtotime( $to . ' -30 days' ) ); } return array( $from, $to ); } private function compare_dates( $from, $to, $mode ) { if ( 'prev' === $mode ) { $days = (int) round( ( strtotime( $to ) - strtotime( $from ) ) / 86400 ) + 1; return array( gmdate( 'Y-m-d', strtotime( $from . ' -' . $days . ' days' ) ), gmdate( 'Y-m-d', strtotime( $from . ' -1 day' ) ) ); } if ( 'yoy' === $mode ) { return array( gmdate( 'Y-m-d', strtotime( $from . ' -1 year' ) ), gmdate( 'Y-m-d', strtotime( $to . ' -1 year' ) ) ); } return array( '', '' ); } private function selection() { $sel = array(); foreach ( self::ORDER as $lvl ) { $sel[ $lvl ] = isset( $_REQUEST[ $lvl ] ) ? array_values( array_filter( array_map( 'sanitize_text_field', (array) wp_unslash( $_REQUEST[ $lvl ] ) ), 'strlen' ) ) : array(); } return $sel; } private function where_and_args( $sel, $consider, $from, $to ) { $w = 'orderdate >= %s AND orderdate <= %s'; $args = array( $from, $to ); foreach ( $consider as $lvl ) { $vals = isset( $sel[ $lvl ] ) ? (array) $sel[ $lvl ] : array(); if ( ! empty( $vals ) ) { $w .= ' AND `' . self::LEVELS[ $lvl ] . '` IN (' . implode( ',', array_fill( 0, count( $vals ), '%s' ) ) . ')'; $args = array_merge( $args, array_values( $vals ) ); } } return array( $w, $args ); } public function ajax_search() { $this->guard(); global $wpdb; $st = $wpdb->prefix . 'aieo_order_stats'; $level = isset( $_REQUEST['level'] ) ? sanitize_key( $_REQUEST['level'] ) : ''; if ( ! isset( self::LEVELS[ $level ] ) ) { wp_send_json( array( 'results' => array() ) ); } $q = isset( $_REQUEST['q'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['q'] ) ) : ''; $sel = $this->selection(); $idx = array_search( $level, self::ORDER, true ); $ancestors = array_slice( self::ORDER, 0, $idx ); $w = '1=1'; $args = array(); foreach ( $ancestors as $alvl ) { $avals = isset( $sel[ $alvl ] ) ? (array) $sel[ $alvl ] : array(); if ( ! empty( $avals ) ) { $w .= ' AND `' . self::LEVELS[ $alvl ] . '` IN (' . implode( ',', array_fill( 0, count( $avals ), '%s' ) ) . ')'; $args = array_merge( $args, array_values( $avals ) ); } } $col = self::LEVELS[ $level ]; $w .= " AND `{$col}` IS NOT NULL AND `{$col}` <> '' AND `{$col}` LIKE %s"; $args[] = '%' . $wpdb->esc_like( $q ) . '%'; $vals = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT `{$col}` FROM {$st} WHERE {$w} ORDER BY `{$col}` LIMIT 200", $args ) ); $out = array(); foreach ( (array) $vals as $v ) { $out[] = array( 'value' => (string) $v, 'label' => (string) $v ); } wp_send_json( array( 'results' => $out ) ); } private function breakdown_expr( $drill ) { $col = '`' . self::LEVELS[ $drill ] . '`'; if ( ! in_array( $drill, array( 'channel', 'source' ), true ) ) { return $col; } return "CASE {$col} WHEN 'paid_google' THEN 'Google - Paid' WHEN 'paid_fb' THEN 'Facebook - Paid' WHEN 'paid_fb-sitelink' THEN 'Facebook - Paid' WHEN 'paid_facebook' THEN 'Facebook - Paid' WHEN 'paid_microsoft' THEN 'Microsoft - Paid' WHEN 'paid_other' THEN 'Paid - Other' WHEN 'organic' THEN 'Organic Search' WHEN 'organic_search' THEN 'Organic Search' WHEN 'google' THEN 'Organic Search' WHEN 'direct' THEN 'Direct' WHEN '(direct)' THEN 'Direct' WHEN 'referral' THEN 'Referral' WHEN 'social' THEN 'Social' WHEN 'ig' THEN 'Instagram' WHEN 'fb' THEN 'Facebook' WHEN 'newsletter' THEN 'Newsletter' WHEN 'aieo_push' THEN 'Push notification' WHEN 'skroutz' THEN 'Skroutz' WHEN 'shopflix' THEN 'Shopflix' ELSE {$col} END"; } public function ajax_explore() { $this->guard(); global $wpdb; $st = $wpdb->prefix . 'aieo_order_stats'; $sel = $this->selection(); list( $from, $to ) = $this->dates(); $drill = null; foreach ( self::ORDER as $lvl ) { if ( empty( $sel[ $lvl ] ) ) { $drill = $lvl; break; } } list( $w, $args ) = $this->where_and_args( $sel, self::ORDER, $from, $to ); $rows = array(); if ( null !== $drill ) { $expr = $this->breakdown_expr( $drill ); $res = $wpdb->get_results( $wpdb->prepare( "SELECT {$expr} AS label, COUNT(*) AS orders, COALESCE(SUM(orderspend),0) AS revenue FROM {$st} WHERE {$w} GROUP BY {$expr} ORDER BY revenue DESC LIMIT 300", $args ) ); foreach ( (array) $res as $r ) { $o = (int) $r->orders; $rev = (float) $r->revenue; $rows[] = array( 'label' => ( '' === (string) $r->label ) ? '(none)' : (string) $r->label, 'orders' => $o, 'revenue' => round( $rev, 2 ), 'aov' => $o > 0 ? round( $rev / $o, 2 ) : 0.0, ); } } $tot = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS orders, COALESCE(SUM(orderspend),0) AS revenue FROM {$st} WHERE {$w}", $args ) ); $total = array( 'orders' => (int) ( $tot->orders ?? 0 ), 'revenue' => round( (float) ( $tot->revenue ?? 0 ), 2 ), ); $compare = isset( $_REQUEST['compare'] ) ? sanitize_key( wp_unslash( $_REQUEST['compare'] ) ) : ''; $cfrom = ''; $cto = ''; if ( in_array( $compare, array( 'prev', 'yoy' ), true ) ) { list( $cfrom, $cto ) = $this->compare_dates( $from, $to, $compare ); list( $cw, $cargs ) = $this->where_and_args( $sel, self::ORDER, $cfrom, $cto ); $ct = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS orders, COALESCE(SUM(orderspend),0) AS revenue FROM {$st} WHERE {$cw}", $cargs ) ); $pco = (int) ( $ct->orders ?? 0 ); $pcr = (float) ( $ct->revenue ?? 0 ); $total['d_orders'] = $pco > 0 ? round( 100 * ( $total['orders'] - $pco ) / $pco, 1 ) : null; $total['d_revenue'] = $pcr > 0 ? round( 100 * ( $total['revenue'] - $pcr ) / $pcr, 1 ) : null; if ( null !== $drill ) { $expr = $this->breakdown_expr( $drill ); $cmap = array(); foreach ( (array) $wpdb->get_results( $wpdb->prepare( "SELECT {$expr} AS label, COUNT(*) AS orders, COALESCE(SUM(orderspend),0) AS revenue FROM {$st} WHERE {$cw} GROUP BY {$expr}", $cargs ) ) as $cr ) { $cmap[ ( '' === (string) $cr->label ) ? '(none)' : (string) $cr->label ] = $cr; } foreach ( $rows as $ri => $r ) { $c = isset( $cmap[ $r['label'] ] ) ? $cmap[ $r['label'] ] : null; $pco = $c ? (int) $c->orders : 0; $pcr = $c ? (float) $c->revenue : 0.0; $rows[ $ri ]['d_orders'] = $pco > 0 ? round( 100 * ( $r['orders'] - $pco ) / $pco, 1 ) : null; $rows[ $ri ]['d_revenue'] = $pcr > 0 ? round( 100 * ( $r['revenue'] - $pcr ) / $pcr, 1 ) : null; } } } wp_send_json_success( array( 'table' => null !== $drill && $rows ? $this->utm_table( self::labels()[ $drill ], $rows, $total, '' !== $cfrom ) : null, 'drill' => $drill, 'drill_label' => $drill ? self::labels()[ $drill ] : '', 'rows' => $rows, 'total' => $total, 'from' => $from, 'to' => $to, 'compare' => $compare, 'cfrom' => $cfrom, 'cto' => $cto, ) ); } private function utm_table( $level, array $rows, array $total, $compare ) { $with = static function ( $cell, $delta ) use ( $compare ) { if ( ! $compare || null === $delta ) { return $cell; } $cell = is_array( $cell ) ? $cell : array( 'html' => esc_html( (string) $cell ) ); $cell['html'] = ( $cell['html'] ?? esc_html( (string) ( $cell['value'] ?? '' ) ) ) . ' ' . esc_html( '(' . ( $delta >= 0 ? '+' : '' ) . number_format_i18n( (float) $delta, 1 ) . '%)' ); $cell['tone'] = $delta >= 0 ? 'success' : 'destructive'; unset( $cell['value'] ); return $cell; }; $out = array(); foreach ( $rows as $i => $r ) { $out[] = array( '_id' => (string) ( $i + 1 ), 'label' => (string) $r['label'], 'orders' => $with( number_format_i18n( (int) $r['orders'] ), $r['d_orders'] ?? null ), 'revenue' => $with( AIEO_Table_Columns::money( $r['revenue'] ), $r['d_revenue'] ?? null ), 'aov' => AIEO_Table_Columns::money( $r['aov'] ), '_s' => array( 'label' => (string) $r['label'], 'orders' => (int) $r['orders'], 'revenue' => (float) $r['revenue'], 'aov' => (float) $r['aov'], ), '_text' => (string) $r['label'], ); } $orders = (int) ( $total['orders'] ?? 0 ); return AIEO_Table::local_answer( 'aieo-utm-table', array( array( 'key' => 'label', 'label' => $level, 'sort_type' => 'text' ), array( 'key' => 'orders', 'label' => __( 'Orders', 'ai-eshop-optimizer' ), 'type' => 'num', 'sort_type' => 'number' ), array( 'key' => 'revenue', 'label' => __( 'Revenue', 'ai-eshop-optimizer' ), 'type' => 'num', 'sort_type' => 'number', 'format' => 'money' ), array( 'key' => 'aov', 'label' => __( 'Avg order', 'ai-eshop-optimizer' ), 'type' => 'num', 'sort_type' => 'number', 'format' => 'money', 'total' => array( 'ratio' => array( 'revenue', 'orders' ) ), ), ), $out, array( 'prefix' => 'utm_', 'size' => 0, 'caption' => __( 'UTM explorer', 'ai-eshop-optimizer' ), 'totals' => array( 'orders' => $orders, 'revenue' => (float) ( $total['revenue'] ?? 0 ), ), ) ); } public function render_subtab() { $this->render_card( true ); } public function render_card( $as_page = false ) { $nonce = wp_create_nonce( self::NONCE ); $def_from = gmdate( 'Y-m-d', strtotime( '-30 days' ) ); $def_to = gmdate( 'Y-m-d' ); if ( defined( 'AIEO_FILE' ) ) { $vp_abs = dirname( AIEO_FILE ) . '/assets/js/aieo-value-picker.js'; wp_enqueue_script( 'aieo-value-picker', plugins_url( 'assets/js/aieo-value-picker.js', AIEO_FILE ), array(), is_readable( $vp_abs ) ? (string) filemtime( $vp_abs ) : '1', true ); } $ajaxurl = esc_attr( admin_url( 'admin-ajax.php' ) ); list( $aieo_utm_open, $aieo_utm_close ) = AIEO_DMM_UI::card( array( 'anchor' => 'utm-explorer-overview', 'icon' => 'hash', 'title' => $as_page ? __( 'Campaign tags', 'ai-eshop-optimizer' ) : __( 'Drill-down by campaign tag', 'ai-eshop-optimizer' ), 'description' => $as_page ? __( 'Orders, revenue and average order value for each campaign tag, narrowed by source, medium, campaign, term and content.', 'ai-eshop-optimizer' ) : __( 'Takes your revenue down to the campaign tags that brought it, from the source and medium to the campaign and its content.', 'ai-eshop-optimizer' ), ) ); echo $aieo_utm_open; echo \Rooster\Admin\Guidance::text( __( 'A stepped drill-down across the five campaign tags. Each is a searchable multi-select — pick any mix of Sources and the lists below narrow to what occurred under them, then Medium, Campaign, Term, Content. The table breaks your current selection down by the next level, with orders, revenue and average order value.', 'ai-eshop-optimizer' ), 'guidance', array( 'question' => __( 'How does the drill-down work?', 'ai-eshop-optimizer' ) ) ); echo '