boot() ) { wp_send_json_error( array( 'message' => 'registry unavailable' ) ); } $res = AIEO_DMM_Contributors::save_rule( array( 'user_id' => isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0, 'match_field' => isset( $_POST['match_field'] ) ? sanitize_key( wp_unslash( $_POST['match_field'] ) ) : 'campaign', 'match_type' => isset( $_POST['match_type'] ) ? sanitize_key( wp_unslash( $_POST['match_type'] ) ) : 'contains', 'match_value' => isset( $_POST['match_value'] ) ? sanitize_text_field( wp_unslash( $_POST['match_value'] ) ) : '', 'label' => isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : '', ) ); if ( empty( $res['ok'] ) ) { wp_send_json_error( $res ); } wp_send_json_success( $res ); } public function ajax_delete() { AIEO_DMM::verify_ajax_request(); if ( ! $this->boot() ) { wp_send_json_error( array( 'message' => 'registry unavailable' ) ); } $ok = AIEO_DMM_Contributors::delete_rule( isset( $_POST['id'] ) ? (int) $_POST['id'] : 0 ); if ( ! $ok ) { wp_send_json_error( array( 'message' => __( 'Could not delete.', 'ai-eshop-optimizer' ) ) ); } wp_send_json_success( array( 'message' => __( 'Removed. Credit is recalculated on the next page load.', 'ai-eshop-optimizer' ) ) ); } public function render_subtab() { if ( ! $this->boot() ) { list( $aieo_ct_open, $aieo_ct_close ) = AIEO_DMM_UI::card( array( 'icon' => 'users-round', 'title' => $this->get_label() ) ); echo $aieo_ct_open; echo \Rooster\Admin\Ui::notice( esc_html__( 'The conversions registry is not loaded.', 'ai-eshop-optimizer' ), 'destructive', array( 'inline' => true ) ); echo $aieo_ct_close; return; } $days = isset( $_GET['pdays'] ) ? (int) $_GET['pdays'] : 90; $until = gmdate( 'Y-m-d H:i:s' ); $since = $days > 0 ? gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS ) : '1970-01-01 00:00:00'; $board = AIEO_DMM_Contributors::leaderboard( $since, $until ); $rec = AIEO_DMM_Contributors::reconcile( $since, $until ); $open = AIEO_DMM_Contributors::unassigned( $since, $until, 60 ); $rules = AIEO_DMM_Contributors::rules(); $band = function_exists( 'rooster_admin_band_open' ); if ( $band ) { rooster_admin_band_open( array( 'cols' => 2 ) ); } $this->card_assign( $open ); $this->card_unclaimed( $open, $rec ); if ( $band ) { rooster_admin_band_close(); rooster_admin_band_open( array( 'cols' => 2 ) ); } $this->card_rules( $rules ); $this->card_board( $board, $rec ); if ( $band ) { rooster_admin_band_close(); } $this->card_macros(); $this->print_script(); } private function card_assign( $open ) { $aieo_ct_form = 'aieo-card-contributors-assign-a-campaign-value-form'; list( $aieo_ct_open, $aieo_ct_close ) = AIEO_DMM_UI::card( array( 'anchor' => 'contributors-assign-a-campaign-value', 'icon' => 'pencil-line', 'title' => __( 'Assign a campaign value', 'ai-eshop-optimizer' ), 'description' => __( 'A contributor is a person or partner whose campaign brings you business. There is no enrolment and no keys: you point an existing campaign value at a WordPress user and all of that value\'s history credits to them immediately. Credit is resolved when the report runs and is never stamped onto the order, so assigning, reassigning and removing are all retroactive and reversible. Reporting only, no commissions or payouts.', 'ai-eshop-optimizer' ), 'tools' => '' . esc_html__( 'Back to Promotions', 'ai-eshop-optimizer' ) . '', 'footer' => \Rooster\Admin\States::save_button( $aieo_ct_form, array( 'label' => __( 'Assign', 'ai-eshop-optimizer' ) ) ), 'col' => 1, ) ); echo $aieo_ct_open; echo \Rooster\Admin\Guidance::text( __( 'Prefer "contains" with the person\'s name. That is not a shortcut: the same campaign is often typed several ways, and on one live site a single contributor\'s campaign appears three ways with the separator lost in one of them. Matching on the name unifies all three, while exact matching would split one person into two.', 'ai-eshop-optimizer' ) ); $aieo_ct_rows = ''; $aieo_ct_rows .= \Rooster\Admin\Ui::field_row( array( 'id' => 'aieo-ct-user', 'label' => __( 'Contributor', 'ai-eshop-optimizer' ), 'control' => '', 'help' => __( 'Any WordPress user can be a contributor. They do not need to log in for their figures to be recorded.', 'ai-eshop-optimizer' ), ) ); $aieo_ct_value = \Rooster\Admin\Ui::input( 'match_value', '', array( 'id' => 'aieo-ct-value', 'medium' => true, 'placeholder' => __( 'e.g. Thanasis', 'ai-eshop-optimizer' ) ) ); if ( $open ) { $aieo_ct_pick_label = __( '— or copy from the worklist (utm_campaign values) —', 'ai-eshop-optimizer' ); $aieo_ct_pick = ''; $aieo_ct_value = '
' . esc_html( $r['match_value'] ) . '',
'added' => substr( (string) $r['created_at'], 0, 10 ),
);
}
AIEO_Table::card( array(
'anchor' => 'contributors-current-mappings',
'icon' => 'table-properties',
'heading' => 'h2',
'title' => __( 'Current mappings', 'ai-eshop-optimizer' ),
'description' => $desc,
'col' => 1,
'narrow' => true,
'page_id' => $this->get_id(),
'row_key' => 'id',
'rows' => $rows,
'filters' => array( array( 'key' => 'who', 'label' => __( 'Contributor', 'ai-eshop-optimizer' ), 'kind' => 'values', 'control' => 'select' ) ),
'empty' => __( 'No mappings yet.', 'ai-eshop-optimizer' ),
'columns' => array(
array( 'key' => 'who', 'label' => __( 'Contributor', 'ai-eshop-optimizer' ) ),
array( 'key' => 'rule', 'label' => __( 'Rule', 'ai-eshop-optimizer' ), 'type' => 'prose' ),
array( 'key' => 'added', 'label' => __( 'Added', 'ai-eshop-optimizer' ) ),
),
'actions' => array(
array(
'id' => 'remove',
'label' => __( 'Remove', 'ai-eshop-optimizer' ),
'description' => __( 'Deletes this mapping and recalculates the credit straight away.', 'ai-eshop-optimizer' ),
'variant' => 'secondary',
'hook' => 'aieo-ct-del',
'destructive' => true,
'confirm_noun' => __( 'mapping', 'ai-eshop-optimizer' ),
'confirm_name' => 'who',
'confirm' => __( 'Credit is recalculated immediately.', 'ai-eshop-optimizer' ),
'data' => array( 'id' => 'id' ),
),
),
) );
}
private function card_board( $board, $rec ) {
$rows = array();
foreach ( (array) $board as $b ) {
$u = get_userdata( (int) $b['user_id'] );
$rows[] = array(
'user' => $u ? $u->display_name : '#' . $b['user_id'],
'leads' => (int) $b['leads'],
'orders' => (int) $b['orders'],
'revenue' => (float) $b['revenue'],
'campaigns' => (int) $b['campaigns'],
'last_seen' => substr( (string) $b['last_seen'], 0, 10 ),
);
}
$recon = \Rooster\Admin\Ui::notice(
'' . esc_html__( 'Reconciliation:', 'ai-eshop-optimizer' ) . ' '
. esc_html( sprintf(
__( '%1$s credited + %2$s unassigned + %3$s not creditable = %4$s conversions carrying an external promotion.', 'ai-eshop-optimizer' ),
number_format_i18n( $rec['credited'] ), number_format_i18n( $rec['unassigned'] ),
number_format_i18n( $rec['rejected'] ), number_format_i18n( $rec['total'] )
) ),
$rec['balanced'] ? 'success' : 'destructive',
array( 'inline' => true )
);
list( $aieo_ct_open, $aieo_ct_close ) = AIEO_DMM_UI::card( array(
'anchor' => 'contributors-what-each-contributor-brought',
'description' => __( 'The leads, orders and revenue credited to each person in this period through their campaign mappings.', 'ai-eshop-optimizer' ),
'icon' => 'banknote',
'title' => __( 'What each contributor brought', 'ai-eshop-optimizer' ),
'flush' => true,
'footer' => $recon,
'col' => 2,
) );
echo $aieo_ct_open;
if ( empty( $rows ) ) {
echo \Rooster\Admin\States::state( 'first-use', array( 'title' => __( 'Nothing credited yet.', 'ai-eshop-optimizer' ) ) );
echo $aieo_ct_close;
return;
}
AIEO_Table::render( array(
'narrow' => true,
'page_id' => $this->get_id(),
'row_key' => 'user',
'rows' => $rows,
'empty' => __( 'Nothing credited yet.', 'ai-eshop-optimizer' ),
'columns' => array(
array( 'key' => 'user', 'label' => __( 'Contributor', 'ai-eshop-optimizer' ) ),
array( 'key' => 'leads', 'label' => __( 'Leads', 'ai-eshop-optimizer' ), 'type' => 'integer' ),
array( 'key' => 'orders', 'label' => __( 'Orders', 'ai-eshop-optimizer' ), 'type' => 'integer' ),
array( 'key' => 'revenue', 'label' => __( 'Revenue', 'ai-eshop-optimizer' ), 'type' => 'money' ),
array( 'key' => 'campaigns', 'label' => __( 'Campaigns', 'ai-eshop-optimizer' ), 'type' => 'integer' ),
array( 'key' => 'last_seen', 'label' => __( 'Last seen', 'ai-eshop-optimizer' ) ),
),
) );
echo $aieo_ct_close;
}
private function card_macros() {
list( $aieo_ct_open, $aieo_ct_close ) = AIEO_DMM_UI::card( array(
'anchor' => 'contributors-how-to-tag-your-ad-urls-and-the-mistake-to-avoid',
'icon' => 'hash',
'title' => __( 'UTM parameters in ad URLs', 'ai-eshop-optimizer' ),
'description' => __(
'A value shown as "unexpanded macro" means the ad platform sent the placeholder text itself instead of substituting the real campaign name. Nothing can be credited from it, because every ad in the account arrives under the identical literal string.',
'ai-eshop-optimizer'
),
) );
echo $aieo_ct_open;
$aieo_ct_braces = __( 'Meta (Facebook / Instagram) uses DOUBLE braces. Google Ads uses SINGLE braces. They are different systems and the syntax is not interchangeable.', 'ai-eshop-optimizer' );
echo \Rooster\Admin\Guidance::text( $aieo_ct_braces, 'guidance', array( 'html' => '' . esc_html( $aieo_ct_braces ) . '', 'question' => __( 'Which braces does each platform use?', 'ai-eshop-optimizer' ) ) );
echo rooster_admin_card_section_html(
__( 'Meta: dot, not underscore', 'ai-eshop-optimizer' ),
array(
'id' => 'aieo-card-contributors-meta-dot-not-underscore',
'description' => __(
'This is the trap. Meta names its campaign, ad-set and ad parameters with a DOT. Written with an underscore they are not recognised and Meta passes the text through untouched. Only site_source_name and placement use no dot.',
'ai-eshop-optimizer'
),
)
);
$macros = array();
foreach ( array(
'{{campaign_name}}' => '{{campaign.name}}',
'{{adset_name}}' => '{{adset.name}}',
'{{adset_id}}' => '{{adset.id}}',
'{{ad_name}}' => '{{ad.name}}',
'{{ad_id}}' => '{{ad.id}}',
) as $bad => $good ) {
$macros[] = array( 'bad' => $bad, 'good' => $good );
}
AIEO_Table::render( array(
'page_id' => $this->get_id(),
'row_key' => 'bad',
'rows' => $macros,
'sortable' => false,
'columns' => array(
array( 'key' => 'bad', 'label' => __( 'Wrong (passes through literally)', 'ai-eshop-optimizer' ), 'type' => 'code', 'tone' => 'danger' ),
array( 'key' => 'good', 'label' => __( 'Correct', 'ai-eshop-optimizer' ), 'type' => 'code', 'tone' => 'success' ),
),
) );
echo '' . esc_html__( 'A complete, correct Meta URL-parameters string:', 'ai-eshop-optimizer' ) . '
'; echo ''
. 'utm_source={{site_source_name}}&utm_medium=paid_social&utm_campaign={{campaign.name}}&utm_content={{ad.name}}&utm_term={{adset.name}}'
. '';
echo \Rooster\Admin\Guidance::text( __(
'{{site_source_name}} resolves to fb, ig, msg or an, which is why utm_source has always worked while the campaign name did not.',
'ai-eshop-optimizer'
), 'guidance', array( 'question' => __( 'Why did only utm_source work?', 'ai-eshop-optimizer' ) ) );
echo rooster_admin_card_section_html(
__( 'Google Ads: single braces (ValueTrack)', 'ai-eshop-optimizer' ),
array( 'id' => 'aieo-card-contributors-google-ads-single-braces' )
);
echo ''
. 'utm_source=google&utm_medium=cpc&utm_campaign={campaignid}&utm_content={creative}&utm_term={keyword}'
. '';
echo \Rooster\Admin\Guidance::text( __(
'Google auto-tagging (gclid) is separate and keeps working regardless. Note that {campaignid} gives you a number, not a name, so a Google campaign is best credited on utm_campaign only if you name campaigns manually.',
'ai-eshop-optimizer'
), 'guidance', array( 'question' => __( 'What about auto-tagging?', 'ai-eshop-optimizer' ) ) );
$aieo_ct_history = \Rooster\Admin\Ui::notice(
esc_html__( 'Conversions already recorded under a literal placeholder cannot be told apart afterwards, because the real campaign name was never sent. Correct the URL parameters in Ads Manager and credit starts from the next click.', 'ai-eshop-optimizer' ),
'warning',
array( 'title' => __( 'Fixing the template does not repair history.', 'ai-eshop-optimizer' ), 'inline' => true )
);
echo $aieo_ct_history;
echo $aieo_ct_close;
}
private function reason_label( $r ) {
$map = array(
'bare_numeric_adset_id' => __( 'ad-set number, not a person', 'ai-eshop-optimizer' ),
'internal_upsell_tagging' => __( 'internal promotion, not external', 'ai-eshop-optimizer' ),
'unexpanded_macro' => __( 'unexpanded macro — see the guide below', 'ai-eshop-optimizer' ),
'marketplace' => __( 'marketplace traffic', 'ai-eshop-optimizer' ),
'literal_undefined' => __( 'literal "undefined"', 'ai-eshop-optimizer' ),
'empty' => __( 'empty', 'ai-eshop-optimizer' ),
);
return isset( $map[ $r ] ) ? $map[ $r ] : str_replace( '_', ' ', $r );
}
}