__('Competitions', 'ai-eshop-optimizer'), 'description' => __('Run prize competitions and giveaways through your existing Gravity Forms — with a ready-made landing page, desktop/mobile creatives, one-click social share links, and (next) participant tracking, winner selection and profiling insights.', 'ai-eshop-optimizer'), 'bullets' => array( __('Associate any Gravity Form as the entry form', 'ai-eshop-optimizer'), __('Branded landing page via a roosterx pattern + block', 'ai-eshop-optimizer'), __('Desktop + mobile creatives, used as the social preview', 'ai-eshop-optimizer'), __('Facebook / TikTok / Instagram share links', 'ai-eshop-optimizer'), __('Participants, winners and profiling metrics', 'ai-eshop-optimizer'), ), ); } public function register_hooks() { if (!class_exists('AIEO_Competition_CPT') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/dmm/competitions/class-aieo-competition-cpt.php')) { require_once AIEO_DIR . 'includes/dmm/competitions/class-aieo-competition-cpt.php'; } if (class_exists('AIEO_Competition_CPT')) { AIEO_Competition_CPT::register(); } if (!class_exists('AIEO_Competition_Participants') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/dmm/competitions/class-aieo-competition-participants.php')) { require_once AIEO_DIR . 'includes/dmm/competitions/class-aieo-competition-participants.php'; } if (class_exists('AIEO_Competition_Participants')) { AIEO_Competition_Participants::register(); } if (!class_exists('AIEO_Competition_Entry_Form') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/dmm/competitions/class-aieo-competition-entry-form.php')) { require_once AIEO_DIR . 'includes/dmm/competitions/class-aieo-competition-entry-form.php'; } if (class_exists('AIEO_Competition_Entry_Form')) { AIEO_Competition_Entry_Form::register(); } if (!class_exists('AIEO_Competition_Analytics') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/dmm/competitions/class-aieo-competition-analytics.php')) { require_once AIEO_DIR . 'includes/dmm/competitions/class-aieo-competition-analytics.php'; } add_action('wp_ajax_aieo_competitions_save', array($this, 'ajax_save')); add_action('wp_ajax_aieo_competitions_settings_save', array($this, 'ajax_save_settings')); add_action('wp_ajax_aieo_competition_draw_winners', array($this, 'ajax_draw_winners')); add_action('wp_ajax_aieo_competition_toggle_winner', array($this, 'ajax_toggle_winner')); if (class_exists('AIEO_Competition_Analytics')) { add_action(AIEO_Competition_Analytics::STATS_REFRESH_HOOK, array('AIEO_Competition_Analytics', 'refresh_stats_cache')); add_action(AIEO_Competition_Analytics::STATS_REFRESH_HOOK . '_once', array('AIEO_Competition_Analytics', 'refresh_stats_cache')); } add_action('wp_ajax_aieo_competitions_refresh_stats', array($this, 'ajax_refresh_stats')); add_action('wp_ajax_aieo_competitions_stats_cadence', array($this, 'ajax_save_stats_cadence')); add_filter('cron_schedules', array($this, 'ensure_cron_schedules')); add_action('admin_init', array($this, 'maybe_schedule_stats')); } const STATS_CADENCES = array('off', 'hourly', 'daily', 'weekly', 'monthly'); public static function get_stats_cadence() { $s = self::get_settings(); $c = isset($s['stats_refresh']) ? (string) $s['stats_refresh'] : 'daily'; return in_array($c, self::STATS_CADENCES, true) ? $c : 'daily'; } public function ensure_cron_schedules($schedules) { if (!is_array($schedules)) { $schedules = array(); } if (!isset($schedules['weekly'])) { $schedules['weekly'] = array('interval' => 7 * DAY_IN_SECONDS, 'display' => __('Once Weekly', 'ai-eshop-optimizer')); } if (!isset($schedules['monthly'])) { $schedules['monthly'] = array('interval' => 30 * DAY_IN_SECONDS, 'display' => __('Once Monthly', 'ai-eshop-optimizer')); } return $schedules; } public function maybe_schedule_stats() { if (!class_exists('AIEO_Competition_Analytics')) { return; } $hook = AIEO_Competition_Analytics::STATS_REFRESH_HOOK; $current = wp_get_schedule($hook); $cadence = self::get_stats_cadence(); if ('off' === $cadence || !self::competitions_exist()) { if (false !== $current) { wp_clear_scheduled_hook($hook); } return; } if ($current === $cadence) { return; } if (false !== $current) { wp_clear_scheduled_hook($hook); } wp_schedule_event(time() + 300, $cadence, $hook); } public function ajax_save_stats_cadence() { if (!check_ajax_referer(self::NONCE, 'nonce', false)) { wp_send_json_error(array('message' => __('Security check failed.', 'ai-eshop-optimizer'))); } if (!current_user_can('edit_posts')) { wp_send_json_error(array('message' => __('You are not allowed to do this.', 'ai-eshop-optimizer'))); } $cadence = isset($_POST['cadence']) ? sanitize_key(wp_unslash($_POST['cadence'])) : 'daily'; if (!in_array($cadence, self::STATS_CADENCES, true)) { $cadence = 'daily'; } $s = self::get_settings(); $s['stats_refresh'] = $cadence; update_option(self::SETTINGS_OPTION, $s, false); $this->maybe_schedule_stats(); wp_send_json_success(array('cadence' => $cadence)); } private static function competitions_exist() { if (!class_exists('AIEO_Competition_CPT')) { return false; } $counts = (array) wp_count_posts(AIEO_Competition_CPT::POST_TYPE); foreach (array('publish', 'draft', 'pending', 'future', 'private') as $st) { if (!empty($counts[$st])) { return true; } } return false; } public function ajax_refresh_stats() { if (!check_ajax_referer(self::NONCE, 'nonce', false)) { wp_send_json_error(array('message' => __('Security check failed.', 'ai-eshop-optimizer'))); } if (!current_user_can('edit_posts') || !class_exists('AIEO_Competition_Analytics')) { wp_send_json_error(array('message' => __('Not allowed.', 'ai-eshop-optimizer'))); } $payload = AIEO_Competition_Analytics::refresh_stats_cache(10); wp_send_json_success(array('generated_at' => $payload['generated_at'])); } public function ajax_draw_winners() { if (!check_ajax_referer(self::NONCE, 'nonce', false)) { wp_send_json_error(array('message' => __('Security check failed.', 'ai-eshop-optimizer'))); } if (!current_user_can('edit_posts') || !class_exists('AIEO_Competition_Participants')) { wp_send_json_error(array('message' => __('Not allowed.', 'ai-eshop-optimizer'))); } $cid = isset($_POST['competition_id']) ? (int) $_POST['competition_id'] : 0; $count = isset($_POST['count']) ? (int) $_POST['count'] : 1; if ($cid <= 0) { wp_send_json_error(array('message' => __('Missing competition.', 'ai-eshop-optimizer'))); } $winners = AIEO_Competition_Participants::draw_winners($cid, $count); wp_send_json_success(array('winners' => self::winners_payload($winners))); } public function ajax_toggle_winner() { if (!check_ajax_referer(self::NONCE, 'nonce', false)) { wp_send_json_error(array('message' => __('Security check failed.', 'ai-eshop-optimizer'))); } if (!current_user_can('edit_posts') || !class_exists('AIEO_Competition_Participants')) { wp_send_json_error(array('message' => __('Not allowed.', 'ai-eshop-optimizer'))); } $pid = isset($_POST['participant_id']) ? (int) $_POST['participant_id'] : 0; $on = !empty($_POST['on']); if ($pid <= 0) { wp_send_json_error(array('message' => __('Missing participant.', 'ai-eshop-optimizer'))); } AIEO_Competition_Participants::set_winner($pid, $on); wp_send_json_success(); } private static function winners_payload($winners) { $out = array(); foreach ((array) $winners as $w) { $name = trim((string) ($w->first_name ?? '') . ' ' . (string) ($w->last_name ?? '')); $out[] = array('pid' => (int) $w->participant_id, 'name' => $name, 'email' => (string) $w->email); } return $out; } const SETTINGS_OPTION = 'aieo_competitions_settings'; public static function get_settings() { $s = get_option(self::SETTINGS_OPTION, array()); return is_array($s) ? $s : array(); } public function enqueue_admin_assets() { if (!self::is_aieo_pro_user()) { return; } wp_enqueue_media(); $css = 'assets/css/aieo-competitions-admin.css'; wp_enqueue_style('aieo-competitions-admin', plugins_url($css, AIEO_FILE), array(), file_exists(AIEO_DIR . $css) ? (string) filemtime(AIEO_DIR . $css) : '1'); $handle = 'aieo-competitions-admin'; $js_rel = 'assets/js/aieo-competitions-admin.js'; $js_abs = defined('AIEO_DIR') ? AIEO_DIR . $js_rel : ''; $js_ver = ($js_abs && file_exists($js_abs)) ? (string) filemtime($js_abs) : (defined('AIEO_VERSION') ? AIEO_VERSION : '1.0'); wp_enqueue_script( $handle, plugins_url($js_rel, AIEO_FILE), array('jquery'), $js_ver, true ); wp_localize_script($handle, 'aieoCompetitions', array( 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce(self::NONCE), 'i18n' => array( 'select' => __('Select image', 'ai-eshop-optimizer'), 'use' => __('Use this image', 'ai-eshop-optimizer'), 'copied' => __('Copied!', 'ai-eshop-optimizer'), 'saving' => __('Saving…', 'ai-eshop-optimizer'), 'saved' => __('Saved.', 'ai-eshop-optimizer'), 'noname' => __('Please enter a competition name.', 'ai-eshop-optimizer'), 'refreshing' => __('Refreshing…', 'ai-eshop-optimizer'), 'error' => __('Error', 'ai-eshop-optimizer'), 'failed' => __('Network error.', 'ai-eshop-optimizer'), ), )); } public function render_subtab() { $this->maybe_schedule_stats(); rooster_admin_band_open(array('cols' => 2)); list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'competitions-new-competition', 'icon' => 'plus', 'title' => __('New competition', 'ai-eshop-optimizer'), 'description' => __('Create a competition, associate its Gravity Form entry form, add a short description and desktop/mobile creatives, then share it. Each competition becomes a landing page you build with the “Competition” pattern (it inherits these details automatically).', 'ai-eshop-optimizer'), 'col' => 1, 'footer' => '' . '', )); echo $open; echo '
'; echo $close; $stats_cache = class_exists('AIEO_Competition_Analytics') ? AIEO_Competition_Analytics::get_cached_stats() : null; $comps = get_posts(array( 'post_type' => AIEO_Competition_CPT::POST_TYPE, 'post_status' => array('publish', 'draft', 'pending', 'future'), 'numberposts' => 200, 'orderby' => 'date', 'order' => 'DESC', )); list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'competitions-your-competitions', 'icon' => 'list', 'title' => __('Your competitions', 'ai-eshop-optimizer'), 'description' => __('Every competition you have created and its status; open one to edit it or draw its winners.', 'ai-eshop-optimizer'), 'col' => 2, 'flush' => true, )); echo $open; $insights_url = add_query_arg( array('page' => AIEO_DMM::MENU_SLUG, 'dmm_tab' => 'competition_insights'), admin_url('admin.php') ); $aieo_comp_where = wp_kses_post(sprintf( __('Entries, entrants who ordered and revenue from entrants are reported under %1$sPerformance → Competition results%2$s.', 'ai-eshop-optimizer'), '', '' )); echo \Rooster\Admin\Guidance::text(wp_strip_all_tags($aieo_comp_where), 'guidance', array('html' => $aieo_comp_where)); if (empty($comps)) { echo \Rooster\Admin\States::state('first-use', array('title' => __('No competitions yet — create one above.', 'ai-eshop-optimizer'))); } else { $rev_map = ($stats_cache && isset($stats_cache['revenue_by_competition'])) ? (array) $stats_cache['revenue_by_competition'] : array(); $sstate_labels = array( 'before' => __('upcoming', 'ai-eshop-optimizer'), 'live' => __('live', 'ai-eshop-optimizer'), 'ended' => __('ended', 'ai-eshop-optimizer'), ); $aieo_cp_rows = array(); foreach ($comps as $c) { $gf_id = (int) get_post_meta($c->ID, AIEO_Competition_CPT::META_GF, true); $sstate = class_exists('AIEO_Competition_CPT') ? AIEO_Competition_CPT::schedule_state($c->ID) : 'live'; $start = AIEO_Competition_CPT::fmt_dt(get_post_meta($c->ID, AIEO_Competition_CPT::META_START, true)); $end = AIEO_Competition_CPT::fmt_dt(get_post_meta($c->ID, AIEO_Competition_CPT::META_END, true)); $m = class_exists('AIEO_Competition_Participants') ? AIEO_Competition_Participants::metrics($c->ID) : array('entries' => 0, 'confirmed' => 0, 'pending' => 0); $inc = isset($rev_map[$c->ID]) ? $rev_map[$c->ID] : null; $share = ''; foreach (AIEO_Competition_CPT::share_links($c->ID) as $net => $url) { if ($url === '') { continue; } $share .= AIEO_Kit::button(ucfirst($net), array( 'variant' => 'ghost', 'size' => 'sm', 'class' => 'aieo-comp-copy', 'attrs' => array( 'data-url' => $url, 'title' => sprintf(__('Copy %s share link', 'ai-eshop-optimizer'), ucfirst($net)), ), )); } $entries_note = (int) $m['confirmed'] . ' ' . __('confirmed', 'ai-eshop-optimizer') . ((int) $m['pending'] > 0 ? ', ' . (int) $m['pending'] . ' ' . __('pending', 'ai-eshop-optimizer') : ''); $aieo_cp_rows[] = array( 'id' => (int) $c->ID, 'name' => get_the_title($c) ?: __('(untitled)', 'ai-eshop-optimizer'), 'edit' => (string) get_edit_post_link($c->ID), 'view' => (string) get_permalink($c->ID), 'dupe' => class_exists('AIEO_Post_Duplicator') ? (string) AIEO_Post_Duplicator::url($c->ID) : '', 'form' => $gf_id ? '#' . $gf_id : '', 'status' => get_post_status($c), 'state' => isset($sstate_labels[$sstate]) ? $sstate_labels[$sstate] : $sstate, 'schedule' => ('' === $start && '' === $end) ? __('Always open', 'ai-eshop-optimizer') : ('' !== $start ? $start : '—') . ' → ' . ('' !== $end ? $end : '—'), 'entries' => (int) $m['entries'], 'entries_sub' => $entries_note, 'income' => ($inc && $inc['revenue'] > 0) ? (float) $inc['revenue'] : null, 'buyers' => ($inc && $inc['revenue'] > 0) ? '(' . (int) $inc['converting'] . ' ' . _n('buyer', 'buyers', (int) $inc['converting'], 'ai-eshop-optimizer') . ')' : '', 'share' => $share, ); } AIEO_Table::render(array( 'page_id' => $this->get_id(), 'row_key' => 'id', 'rows' => $aieo_cp_rows, 'empty' => __('No competitions yet — create one above.', 'ai-eshop-optimizer'), 'columns' => array( array('key' => 'name', 'label' => __('Name', 'ai-eshop-optimizer'), 'type' => 'link', 'format' => array('href' => 'edit')), array('key' => 'form', 'label' => __('Form', 'ai-eshop-optimizer'), 'type' => 'id', 'empty' => __('— none —', 'ai-eshop-optimizer')), array('key' => 'status', 'label' => __('Status', 'ai-eshop-optimizer'), 'type' => 'badge', 'subline' => 'state'), array('key' => 'schedule', 'label' => __('Schedule', 'ai-eshop-optimizer'), 'nowrap' => true), array('key' => 'entries', 'label' => __('Entries', 'ai-eshop-optimizer'), 'type' => 'integer', 'subline' => 'entries_sub'), array('key' => 'income', 'label' => __('Income', 'ai-eshop-optimizer'), 'type' => 'money', 'suffix' => 'buyers', 'nowrap' => true), array('key' => 'share', 'label' => __('Share', 'ai-eshop-optimizer'), 'type' => 'html', 'sortable' => false), ), 'actions' => array( array('id' => 'edit', 'label' => __('Edit', 'ai-eshop-optimizer'), 'href' => static function ($r) { return $r['edit']; }), array('id' => 'view', 'label' => __('View', 'ai-eshop-optimizer'), 'external' => true, 'href' => static function ($r) { return $r['view']; }), array( 'id' => 'duplicate', 'label' => __('Duplicate', 'ai-eshop-optimizer'), 'visible' => static function ($r) { return '' !== $r['dupe']; }, 'href' => static function ($r) { return $r['dupe']; }, ), ), )); } echo $close; $cset = self::get_settings(); $msg_soon = isset($cset['msg_soon']) ? (string) $cset['msg_soon'] : ''; $msg_ended = isset($cset['msg_ended']) ? (string) $cset['msg_ended'] : ''; $msg_announce = isset($cset['msg_announce']) ? (string) $cset['msg_announce'] : ''; $msg_form = 'aieo-card-competitions-page-messages-form'; list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'competitions-page-messages', 'icon' => 'message-square-text', 'title' => __('Competition messages and colours', 'ai-eshop-optimizer'), 'description' => __('Shown on the competition page to visitors. These apply to ALL competitions. Leave a field empty to use the built-in (translatable) default.', 'ai-eshop-optimizer'), 'col' => 2, 'footer' => class_exists('\Rooster\Admin\States') ? \Rooster\Admin\States::save_button($msg_form) : '', )); echo $open; ob_start(); echo '' . $pointer . '
' : '', )); echo $open; echo '