array(__('SOFT1 Customer Name', 'ai-eshop-optimizer'), 'text'), 'soft1_customer_id' => array(__('SOFT1 Customer ID', 'ai-eshop-optimizer'), 'int'), 'soft1_customer_ids' => array(__('SOFT1 Customer IDs (all locations)', 'ai-eshop-optimizer'), 'text'), 'soft1_loyalty_id' => array(__('SOFT1 Loyalty ID', 'ai-eshop-optimizer'), 'int'), 'soft1_turnover' => array(__('SOFT1 Turnover', 'ai-eshop-optimizer'), 'decimal'), 'soft1_loyalty_points' => array(__('SOFT1 Loyalty Points', 'ai-eshop-optimizer'), 'int'), 'soft1_loyalty_points_total' => array(__('SOFT1 Loyalty Points Total', 'ai-eshop-optimizer'), 'int'), 'eshop_loyalty_points' => array(__('Eshop Loyalty Points', 'ai-eshop-optimizer'), 'decimal'), 'soft1_customer_locations' => array(__('SOFT1 Customer Locations', 'ai-eshop-optimizer'), 'text'), 'soft1_latest_location' => array(__('SOFT1 Latest Location', 'ai-eshop-optimizer'), 'text'), 'soft1_recorded_speciality' => array(__('SOFT1 Recorded Speciality', 'ai-eshop-optimizer'), 'text'), 'soft1_latest_transaction_date' => array(__('SOFT1 Latest Transaction Date', 'ai-eshop-optimizer'), 'datetime'), 'soft1_first_transaction_date' => array(__('SOFT1 First Transaction Date', 'ai-eshop-optimizer'), 'datetime'), 'eshop_latest_transaction_date' => array(__('Eshop Latest Transaction Date', 'ai-eshop-optimizer'), 'datetime'), ); } public function get_id() { return 'user_vitals'; } public function get_icon() { return 'id-card'; } public function get_label() { return __('Customer ERP fields', 'ai-eshop-optimizer'); } public function get_priority() { return 10; } public function get_group() { return 'customer_profiling'; } public function register_hooks() { if (function_exists('aieo_boot_schema_should_run') && aieo_boot_schema_should_run('user_vitals')) { $schema = AIEO_DIR . 'includes/dmm/sql/user-vitals-schema.php'; if (file_exists($schema)) { require_once $schema; } if (function_exists('aieo_install_user_vitals_schema')) { aieo_install_user_vitals_schema(); aieo_boot_schema_done('user_vitals'); } } add_action('wp_ajax_aieo_user_vitals_scan', array($this, 'ajax_scan')); add_action('wp_ajax_aieo_user_vitals_migrate', array($this, 'ajax_migrate')); add_action('wp_ajax_aieo_user_vitals_save_loyalty', array($this, 'ajax_save_loyalty')); add_action('wp_ajax_aieo_user_vitals_save_gf', array($this, 'ajax_save_gf')); $loyalty = AIEO_DIR . 'includes/class-aieo-loyalty-connection.php'; if (file_exists($loyalty)) { require_once $loyalty; } if (class_exists('AIEO_Loyalty_Connection') && empty($GLOBALS['aieo_loyalty_connection_booted'])) { $GLOBALS['aieo_loyalty_connection_booted'] = true; new AIEO_Loyalty_Connection(); } $repeat_order = AIEO_DIR . 'includes/class-aieo-repeat-order.php'; if (file_exists($repeat_order)) { require_once $repeat_order; } if (class_exists('AIEO_Repeat_Order') && empty($GLOBALS['aieo_repeat_order_booted'])) { $GLOBALS['aieo_repeat_order_booted'] = true; new AIEO_Repeat_Order(); } $pro_tab = AIEO_DIR . 'includes/class-aieo-pro-account-tab.php'; if (file_exists($pro_tab)) { require_once $pro_tab; } if (class_exists('AIEO_Pro_Account_Tab') && empty($GLOBALS['aieo_pro_account_tab_booted'])) { $GLOBALS['aieo_pro_account_tab_booted'] = true; new AIEO_Pro_Account_Tab(); } add_action('show_user_profile', array($this, 'render_profile_fields')); add_action('edit_user_profile', array($this, 'render_profile_fields')); add_action('personal_options_update', array($this, 'save_profile_fields')); add_action('edit_user_profile_update', array($this, 'save_profile_fields')); add_action('woocommerce_order_status_changed', array($this, 'on_order_status_changed'), 10, 4); add_action('woocommerce_new_order', array($this, 'on_new_order'), 10, 2); } public static function txn_statuses() { return array('pending', 'processing', 'on-hold', 'completed'); } public function on_order_status_changed($order_id, $from, $to, $order = null) { if (in_array((string) $to, self::txn_statuses(), true)) { $this->touch_eshop_latest_txn($order instanceof WC_Order ? $order : wc_get_order($order_id)); } } public function on_new_order($order_id, $order = null) { if (!($order instanceof WC_Order)) { $order = function_exists('wc_get_order') ? wc_get_order($order_id) : null; } if ($order instanceof WC_Order && in_array($order->get_status(), self::txn_statuses(), true)) { $this->touch_eshop_latest_txn($order); } } private function touch_eshop_latest_txn($order) { global $wpdb; if (!($order instanceof WC_Order)) { return; } $uid = (int) $order->get_customer_id(); if ($uid <= 0) { return; } $created = $order->get_date_created(); if (!$created) { return; } $gmt = clone $created; $gmt->setTimezone(new DateTimeZone('UTC')); $val = $gmt->format('Y-m-d H:i:s'); $tbl = $this->table(); if (function_exists('aieo_dmm_table_exists') && !aieo_dmm_table_exists($tbl)) { return; } $wpdb->query($wpdb->prepare( "INSERT INTO `{$tbl}` (`user_id`, `eshop_latest_transaction_date`) VALUES (%d, %s) ON DUPLICATE KEY UPDATE `eshop_latest_transaction_date` = IF( `eshop_latest_transaction_date` IS NULL OR `eshop_latest_transaction_date` < VALUES(`eshop_latest_transaction_date`), VALUES(`eshop_latest_transaction_date`), `eshop_latest_transaction_date` )", $uid, $val )); } private function table() { global $wpdb; return $wpdb->prefix . self::TABLE; } private function src_table() { global $wpdb; return $wpdb->prefix . self::SRC; } private function src_exists() { global $wpdb; return (bool) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $this->src_table())); } private function has_data_sql() { return "(pu.soft1_customer_id IS NOT NULL OR pu.soft1_loyalty_id IS NOT NULL OR pu.soft1_turnover IS NOT NULL" . " OR pu.soft1_loyalty_points IS NOT NULL OR pu.soft1_loyalty_points_total IS NOT NULL OR pu.eshop_loyalty_points IS NOT NULL" . " OR NULLIF(pu.soft1_customer_name,'') IS NOT NULL OR NULLIF(pu.soft1_customer_ids,'') IS NOT NULL" . " OR NULLIF(pu.soft1_recorded_speciality,'') IS NOT NULL OR NULLIF(pu.soft1_customer_locations,'') IS NOT NULL" . " OR NULLIF(pu.soft1_latest_transaction_date,'') IS NOT NULL)"; } public function ajax_scan() { AIEO_DMM::verify_ajax_request(); global $wpdb; if (!$this->src_exists()) { wp_send_json_error(array('message' => __('Legacy Pods user table (wp_pods_user) was not found — nothing to migrate.', 'ai-eshop-optimizer'))); } $src = $this->src_table(); $tbl = $this->table(); $eligible = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$src} pu JOIN {$wpdb->users} u ON u.ID = pu.id WHERE " . $this->has_data_sql()); $orphans = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$src} pu LEFT JOIN {$wpdb->users} u ON u.ID = pu.id WHERE u.ID IS NULL AND " . $this->has_data_sql()); $existing = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl}"); wp_send_json_success(array( 'eligible' => $eligible, 'orphans' => $orphans, 'existing' => $existing, 'message' => sprintf( __('✓ %1$s users with ERP/SOFT1 data ready · %2$s already in the table · %3$s skipped (no matching WP user).', 'ai-eshop-optimizer'), number_format_i18n($eligible), number_format_i18n($existing), number_format_i18n($orphans) ), )); } public function ajax_migrate() { AIEO_DMM::verify_ajax_request(); global $wpdb; if (!$this->src_exists()) { wp_send_json_error(array('message' => __('Legacy Pods user table not found.', 'ai-eshop-optimizer'))); } $src = $this->src_table(); $tbl = $this->table(); $mode = isset($_POST['mode']) ? sanitize_key((string) wp_unslash($_POST['mode'])) : 'batch'; if ($mode === 'init') { $ids = $wpdb->get_col("SELECT pu.id FROM {$src} pu JOIN {$wpdb->users} u ON u.ID = pu.id WHERE " . $this->has_data_sql() . " ORDER BY pu.id ASC"); $ids = array_values(array_filter(array_map('intval', (array) $ids))); set_transient(self::TR_IDS, $ids, 2 * HOUR_IN_SECONDS); wp_send_json_success(array('total' => count($ids))); } $offset = isset($_POST['offset']) ? max(0, (int) $_POST['offset']) : 0; $size = 1000; $ids = get_transient(self::TR_IDS); if (!is_array($ids)) { wp_send_json_error(array('message' => __('Migration session expired — click Migrate again.', 'ai-eshop-optimizer'))); } $slice = array_slice($ids, $offset, $size); if (empty($slice)) { wp_send_json_success(array('done' => true, 'processed' => count($ids), 'total' => count($ids), 'batch' => 0, 'total_rows' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl}"))); } $in = implode(',', array_map('intval', $slice)); $d = static function ($c) { return "(CASE WHEN {$c} REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}' THEN {$c} ELSE NULL END)"; }; $affected = $wpdb->query( "REPLACE INTO {$tbl} (user_id, soft1_customer_name, soft1_customer_id, soft1_customer_ids, soft1_loyalty_id, soft1_turnover, soft1_loyalty_points, soft1_loyalty_points_total, eshop_loyalty_points, soft1_customer_locations, soft1_latest_location, soft1_recorded_speciality, soft1_latest_transaction_date, soft1_first_transaction_date, eshop_latest_transaction_date, migrated_at) SELECT pu.id, NULLIF(pu.soft1_customer_name,''), pu.soft1_customer_id, NULLIF(pu.soft1_customer_ids,''), pu.soft1_loyalty_id, pu.soft1_turnover, pu.soft1_loyalty_points, pu.soft1_loyalty_points_total, pu.eshop_loyalty_points, NULLIF(pu.soft1_customer_locations,''), NULLIF(pu.soft1_latest_location,''), NULLIF(pu.soft1_recorded_speciality,''), {$d('pu.soft1_latest_transaction_date')}, {$d('pu.soft1_first_transaction_date')}, {$d('pu.eshop_latest_transaction_date')}, UTC_TIMESTAMP() FROM {$src} pu WHERE pu.id IN ({$in})" ); $processed = $offset + count($slice); $done = ($processed >= count($ids)); $resp = array('done' => $done, 'processed' => $processed, 'total' => count($ids), 'batch' => (int) $affected); if ($done) { $resp['total_rows'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl}"); } wp_send_json_success($resp); } public function render_profile_fields($user) { if (!current_user_can('manage_options') || !is_object($user) || empty($user->ID)) { return; } global $wpdb; $tbl = $this->table(); if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl))) { return; } $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$tbl} WHERE user_id = %d", (int) $user->ID), ARRAY_A); echo '

' . esc_html__('ERP / SOFT1 vitals', 'ai-eshop-optimizer') . '

'; echo '

' . sprintf( esc_html__('Synced from the ERP (SOFT1) and stored in %s. Editing here updates that table directly; a future ERP sync may overwrite these values.', 'ai-eshop-optimizer'), '' . esc_html($wpdb->prefix . self::TABLE) . '' ) . '

'; echo ''; foreach (self::fields() as $col => $meta) { $val = ($row && isset($row[$col]) && $row[$col] !== null) ? (string) $row[$col] : ''; $type = $meta[1]; $itype = ($type === 'int' || $type === 'decimal') ? 'number' : 'text'; $extra = ($type === 'decimal') ? ' step="0.01"' : (($type === 'int') ? ' step="1"' : ''); $extra .= ($type === 'datetime') ? ' placeholder="YYYY-MM-DD HH:MM:SS"' : ''; $id = 'aieo_uv_' . $col; echo ''; echo ''; echo ''; echo ''; } echo ''; } public function save_profile_fields($user_id) { if (!current_user_can('manage_options') || !current_user_can('edit_user', $user_id)) { return; } if (!isset($_POST['aieo_uv_soft1_customer_id']) && !isset($_POST['aieo_uv_soft1_customer_name'])) { return; } global $wpdb; $tbl = $this->table(); if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl))) { return; } $data = array('user_id' => (int) $user_id); $fmt = array('%d'); foreach (self::fields() as $col => $meta) { $key = 'aieo_uv_' . $col; $raw = isset($_POST[$key]) ? trim((string) wp_unslash($_POST[$key])) : ''; $type = $meta[1]; if ($raw === '') { $data[$col] = null; $fmt[] = '%s'; } elseif ($type === 'int') { $data[$col] = (int) preg_replace('/[^0-9\-]/', '', $raw); $fmt[] = '%d'; } elseif ($type === 'decimal') { $data[$col] = (float) str_replace(',', '.', preg_replace('/[^0-9,.\-]/', '', $raw)); $fmt[] = '%f'; } elseif ($type === 'datetime') { $data[$col] = preg_match('/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$/', $raw) ? $raw : null; $fmt[] = '%s'; } else { $data[$col] = sanitize_text_field($raw); $fmt[] = '%s'; } } $data['migrated_at'] = $wpdb->get_var($wpdb->prepare("SELECT migrated_at FROM {$tbl} WHERE user_id = %d", (int) $user_id)); $fmt[] = '%s'; $wpdb->replace($tbl, $data, $fmt); } public function ajax_save_loyalty() { AIEO_DMM::verify_ajax_request(); $loyalty_role = isset($_POST['loyalty_role']) ? sanitize_key(wp_unslash($_POST['loyalty_role'])) : ''; if ($loyalty_role !== '' && !AIEO_Safe_Roles::is_safe($loyalty_role)) { wp_send_json_error(array('message' => self::unsafe_role_message($loyalty_role))); } AIEO_DMM_Settings::update(array( 'loyalty_role' => $loyalty_role, 'loyalty_customers_table' => isset($_POST['loyalty_customers_table']) ? sanitize_key(wp_unslash($_POST['loyalty_customers_table'])) : '', 'loyalty_my_account' => !empty($_POST['loyalty_my_account']), 'loyalty_my_account_label' => isset($_POST['loyalty_my_account_label']) ? sanitize_text_field(wp_unslash($_POST['loyalty_my_account_label'])) : '', )); flush_rewrite_rules(false); wp_send_json_success(array('message' => __('✓ Saved.', 'ai-eshop-optimizer'))); } private static function unsafe_role_message($role) { if (in_array('missing', AIEO_Safe_Roles::why_not($role), true)) { return sprintf(__('Not saved: this site has no role “%s”.', 'ai-eshop-optimizer'), $role); } return sprintf(__('Not saved: “%1$s” gives power over the shop (%2$s), so it cannot be handed out on its own. Choose a customer role.', 'ai-eshop-optimizer'), $role, implode(', ', array_slice(AIEO_Safe_Roles::why_not($role), 0, 5))); } public function render_loyalty_card() { global $wpdb; $role = (string) AIEO_DMM_Settings::get('loyalty_role', ''); if ($role === '') { $role = (string) get_option('s1erp_prod_woo_loyalty_role', 'customer_loyalty'); } $table = (string) AIEO_DMM_Settings::get('loyalty_customers_table', ''); if ($table === '') { $table = (string) get_option('s1erp_site_customers_table', ''); } $mya = (bool) AIEO_DMM_Settings::get('loyalty_my_account', false); $label = (string) AIEO_DMM_Settings::get('loyalty_my_account_label', ''); $label_hint = __('Bonus Card', 'ai-eshop-optimizer'); $rows = ($table !== '' && $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) ? (int) $wpdb->get_var("SELECT COUNT(*) FROM `{$table}`") : -1; $aieo_uv_form = 'aieo-card-user-vitals-loyalty-connection-bonus-card-form'; $aieo_uv_rows = \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-loy-role', 'label' => __('Loyalty role slug', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('loyalty_role', $role, array('id' => 'aieo-loy-role', 'medium' => true)), 'help' => __('A customer role, one that gives no power over the shop; the Join refuses any other.', 'ai-eshop-optimizer'), )); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-loy-table', 'label' => __('ERP customers table', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('loyalty_customers_table', $table, array('id' => 'aieo-loy-table')), 'help' => $rows >= 0 ? sprintf(__('Found — %s rows.', 'ai-eshop-optimizer'), number_format_i18n($rows)) : __('Table not found / empty — the meta-connector ERP customer-import populates it.', 'ai-eshop-optimizer'), )); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-loy-mya', 'label' => __('My Account menu', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::toggle('loyalty_my_account', $mya, array('id' => 'aieo-loy-mya', 'label' => __('Add a Bonus tab to the WooCommerce My Account page', 'ai-eshop-optimizer'))), )); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-loy-label', 'label' => __('My Account tab label', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('loyalty_my_account_label', $label, array('id' => 'aieo-loy-label', 'medium' => true, 'placeholder' => $label_hint)), )); list($aieo_uv_open, $aieo_uv_close) = AIEO_DMM_UI::card(array( 'anchor' => 'user-vitals-loyalty-connection-bonus-card', 'icon' => 'credit-card', 'title' => __('Bonus Card connection', 'ai-eshop-optimizer'), 'description' => __('A logged-in customer enters their Bonus-card number + the email/phone they gave in-store; on a match it copies their loyalty fields into the vitals table and grants the loyalty role (immediate access). Drop it in with the “Rooster One Loyalty Connection” block or this shortcode:', 'ai-eshop-optimizer') . ' [aieo_loyalty_information]', 'footer' => \Rooster\Admin\States::save_button($aieo_uv_form), 'col' => 1, )); echo $aieo_uv_open; echo AIEO_DMM_UI::card_form('aieo_user_vitals_save_loyalty', 'user_vitals_loyalty', $aieo_uv_form, '
' . $aieo_uv_rows . '
'); echo $aieo_uv_close; } public function ajax_save_gf() { AIEO_DMM::verify_ajax_request(); $map = array(); if (isset($_POST['gf_role_map']) && is_array($_POST['gf_role_map'])) { foreach (wp_unslash($_POST['gf_role_map']) as $slug => $val) { $slug = sanitize_key((string) $slug); $iv = absint($val); if ($slug !== '' && $iv > 0) { $map[$slug] = $iv; } } } foreach (array_merge(array_keys($map), array( isset($_POST['pro_signup_role_professional']) ? sanitize_key(wp_unslash($_POST['pro_signup_role_professional'])) : '', isset($_POST['pro_signup_role_student']) ? sanitize_key(wp_unslash($_POST['pro_signup_role_student'])) : '', )) as $aieo_uv_role) { if ($aieo_uv_role !== '' && !AIEO_Safe_Roles::is_safe((string) $aieo_uv_role)) { wp_send_json_error(array('message' => self::unsafe_role_message((string) $aieo_uv_role))); } } AIEO_DMM_Settings::update(array( 'gf_registration_form_id' => isset($_POST['gf_registration_form_id']) ? absint(wp_unslash($_POST['gf_registration_form_id'])) : 0, 'gf_update_form_id' => isset($_POST['gf_update_form_id']) ? absint(wp_unslash($_POST['gf_update_form_id'])) : 0, 'gf_checkbox_field_id' => isset($_POST['gf_checkbox_field_id']) ? max(1, absint(wp_unslash($_POST['gf_checkbox_field_id']))) : 11, 'gf_role_map' => $map, 'gf_professionals_page_id' => isset($_POST['gf_professionals_page_id']) ? absint(wp_unslash($_POST['gf_professionals_page_id'])) : 0, 'gf_prompt_form_id' => isset($_POST['gf_prompt_form_id']) ? absint(wp_unslash($_POST['gf_prompt_form_id'])) : 0, 'gf_prompt_pro_field_id' => isset($_POST['gf_prompt_pro_field_id']) ? max(1, absint(wp_unslash($_POST['gf_prompt_pro_field_id']))) : 51, 'gf_prompt_member_field_id' => isset($_POST['gf_prompt_member_field_id']) ? max(1, absint(wp_unslash($_POST['gf_prompt_member_field_id']))) : 48, 'gf_intro_logged_in' => isset($_POST['gf_intro_logged_in']) ? wp_kses_post(wp_unslash($_POST['gf_intro_logged_in'])) : '', 'gf_intro_logged_out' => isset($_POST['gf_intro_logged_out']) ? wp_kses_post(wp_unslash($_POST['gf_intro_logged_out'])) : '', 'pro_signup_form' => isset($_POST['pro_signup_form']) ? sanitize_key(wp_unslash($_POST['pro_signup_form'])) : '', 'pro_signup_role_professional' => isset($_POST['pro_signup_role_professional']) ? sanitize_key(wp_unslash($_POST['pro_signup_role_professional'])) : '', 'pro_signup_role_student' => isset($_POST['pro_signup_role_student']) ? sanitize_key(wp_unslash($_POST['pro_signup_role_student'])) : '', )); wp_send_json_success(array('message' => __('✓ Saved.', 'ai-eshop-optimizer'))); } public function render_gf_card() { $s = class_exists('AIEO_DMM_Settings') ? AIEO_DMM_Settings::get_all() : array(); $enabled = (isset($s['enabled_roles']) && is_array($s['enabled_roles'])) ? $s['enabled_roles'] : array(); $map = (isset($s['gf_role_map']) && is_array($s['gf_role_map'])) ? $s['gf_role_map'] : array(); if (!function_exists('get_editable_roles')) { require_once ABSPATH . 'wp-admin/includes/user.php'; } $all = function_exists('get_editable_roles') ? get_editable_roles() : array(); $gf_active = class_exists('GFAPI'); $forms = array(); if ($gf_active) { foreach ((array) GFAPI::get_forms(true, false) as $f) { if (isset($f['id'])) { $forms[] = array('id' => (int) $f['id'], 'title' => isset($f['title']) ? (string) $f['title'] : ('#' . (int) $f['id'])); } } } $sel = static function ($name, $id, $cur) use ($forms) { $cur = (int) $cur; if (empty($forms)) { return \Rooster\Admin\Ui::input($name, (string) $cur, array('id' => $id, 'type' => 'number', 'short' => true, 'min' => 0)); } $options = array('0' => '— ' . __('disabled', 'ai-eshop-optimizer') . ' —'); foreach ($forms as $f) { $options[(string) (int) $f['id']] = '#' . (int) $f['id'] . ' — ' . $f['title']; } return \Rooster\Admin\Ui::select($name, $options, (string) $cur, array('id' => $id)); }; $aieo_uv_form = 'aieo-card-user-vitals-professional-signup-gravity-forms-form'; $aieo_uv_roles = array('' => '— ' . __('none', 'ai-eshop-optimizer') . ' —'); foreach (array_filter($enabled, array('AIEO_Safe_Roles', 'is_safe')) as $aieo_uv_slug) { $aieo_uv_roles[(string) $aieo_uv_slug] = isset($all[$aieo_uv_slug]['name']) ? translate_user_role($all[$aieo_uv_slug]['name']) : (string) $aieo_uv_slug; } $aieo_uv_rows = \Rooster\Admin\Ui::field_row(array('id' => 'aieo-pro-form', 'label' => __('Signup form', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::select('pro_signup_form', array('aieo' => __('Rooster One’s application forms', 'ai-eshop-optimizer'), 'gravity' => __('Gravity Forms', 'ai-eshop-optimizer')), AIEO_Pro_Signup::provider(), array('id' => 'aieo-pro-form')))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-pro-role', 'label' => __('Role of an approved professional', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::select('pro_signup_role_professional', $aieo_uv_roles, AIEO_Pro_Signup::role('professional'), array('id' => 'aieo-pro-role')))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-pro-role-student', 'label' => __('Role of an approved student', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::select('pro_signup_role_student', $aieo_uv_roles, AIEO_Pro_Signup::role('student'), array('id' => 'aieo-pro-role-student')))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-gf-reg', 'label' => __('Registration form', 'ai-eshop-optimizer'), 'control' => $sel('gf_registration_form_id', 'aieo-gf-reg', $s['gf_registration_form_id'] ?? 0))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-gf-upd', 'label' => __('Update form', 'ai-eshop-optimizer'), 'control' => $sel('gf_update_form_id', 'aieo-gf-upd', $s['gf_update_form_id'] ?? 0))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-gf-cb', 'label' => __('Checkbox field ID', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('gf_checkbox_field_id', (string) ($s['gf_checkbox_field_id'] ?? 11), array('id' => 'aieo-gf-cb', 'type' => 'number', 'short' => true, 'min' => 1)))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-gf-page', 'label' => __('Membership page ID', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('gf_professionals_page_id', (string) ($s['gf_professionals_page_id'] ?? 0), array('id' => 'aieo-gf-page', 'type' => 'number', 'short' => true, 'min' => 0)))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-gf-prompt', 'label' => __('Prompt form', 'ai-eshop-optimizer'), 'control' => $sel('gf_prompt_form_id', 'aieo-gf-prompt', $s['gf_prompt_form_id'] ?? 0))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-gf-pro', 'label' => __('“Is pro?” / “Already enrolled?” field IDs', 'ai-eshop-optimizer'), 'joiner' => '/', 'parts' => array( array('label' => __('Is pro?', 'ai-eshop-optimizer'), 'id' => 'aieo-gf-pro', 'control' => \Rooster\Admin\Ui::input('gf_prompt_pro_field_id', (string) ($s['gf_prompt_pro_field_id'] ?? 51), array('id' => 'aieo-gf-pro', 'type' => 'number', 'short' => true, 'min' => 1))), array('label' => __('Already enrolled?', 'ai-eshop-optimizer'), 'id' => 'aieo-gf-mem', 'control' => \Rooster\Admin\Ui::input('gf_prompt_member_field_id', (string) ($s['gf_prompt_member_field_id'] ?? 48), array('id' => 'aieo-gf-mem', 'type' => 'number', 'short' => true, 'min' => 1))), ), )); $aieo_uv_map = ''; if (empty($enabled)) { $aieo_uv_map = \Rooster\Admin\Ui::notice(esc_html__('Enable roles on the Role Pricing tab first.', 'ai-eshop-optimizer'), 'warning', array('inline' => true)); } else { $uv_gf_rows = array(); foreach ($enabled as $slug) { $uv_gf_rows[] = array( 'slug' => (string) $slug, 'label' => isset($all[$slug]['name']) ? translate_user_role($all[$slug]['name']) : (string) $slug, 'value' => (int) ($map[$slug] ?? 0), ); } $aieo_uv_map = AIEO_Table::get_html(array( 'columns' => array( array( 'key' => 'slug', 'label' => __('Role', 'ai-eshop-optimizer'), 'type' => 'code', 'suffix' => 'label', ), array( 'key' => 'value', 'label' => __('GF value', 'ai-eshop-optimizer'), 'type' => 'control', 'width' => '120px', 'format' => array( 'render' => static function ($row) { return \Rooster\Admin\Ui::input('gf_role_map[' . $row['slug'] . ']', (string) $row['value'], array('id' => 'aieo-gf-map-' . $row['slug'], 'type' => 'number', 'xs' => true, 'min' => 0, 'aria-label' => sprintf(__('GF value for %s', 'ai-eshop-optimizer'), $row['label']))); }, ), ), ), 'rows' => $uv_gf_rows, 'row_key' => 'slug', 'page_id' => 'user_vitals', )); } list($aieo_uv_map_open, $aieo_uv_map_close) = AIEO_DMM_UI::card(array('anchor' => 'user-vitals-role-gf-checkbox-value', 'title' => __('Role → GF checkbox value', 'ai-eshop-optimizer'), 'description' => __('Matches each enabled role to the value of its checkbox in the Gravity Forms signup form.', 'ai-eshop-optimizer'), 'level' => 3, 'flush' => !empty($enabled))); $aieo_uv_rows .= $aieo_uv_map_open . $aieo_uv_map . $aieo_uv_map_close; $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-gf-intro-in', 'label' => __('Logged-in intro', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::textarea('gf_intro_logged_in', (string) ($s['gf_intro_logged_in'] ?? ''), array('id' => 'aieo-gf-intro-in', 'rows' => 2, 'code' => true)))); $aieo_uv_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-gf-intro-out', 'label' => __('Logged-out intro', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::textarea('gf_intro_logged_out', (string) ($s['gf_intro_logged_out'] ?? ''), array('id' => 'aieo-gf-intro-out', 'rows' => 2, 'code' => true)))); list($aieo_uv_open, $aieo_uv_close) = AIEO_DMM_UI::card(array( 'anchor' => 'user-vitals-professional-signup-gravity-forms', 'icon' => 'user-plus', 'title' => __('Professional signup and roles', 'ai-eshop-optimizer'), 'description' => __('Professional and student account applications, on Rooster One’s own forms (approved on each form’s Answers) or on Gravity Forms. Place them with the “Rooster One Professional Signup” block or the shortcode', 'ai-eshop-optimizer') . ' [aieo_pro_signup kind="professional"].', 'footer' => \Rooster\Admin\States::save_button($aieo_uv_form), 'col' => 2, )); echo $aieo_uv_open; foreach (AIEO_Pro_Signup::waiting_counts() as $aieo_uv_wait) { if ($aieo_uv_wait['count'] < 1) { continue; } $aieo_uv_text = sprintf( _n('%1$s: %2$d application waits for approval.', '%1$s: %2$d applications wait for approval.', $aieo_uv_wait['count'], 'ai-eshop-optimizer'), get_the_title($aieo_uv_wait['form_id']), $aieo_uv_wait['count'] ); \Rooster\Admin\Markup::out(\Rooster\Admin\Ui::notice(esc_html($aieo_uv_text) . ' ' . esc_html__('Review them', 'ai-eshop-optimizer') . '', 'info', array('inline' => true))); } if (!$gf_active && 'gravity' === AIEO_Pro_Signup::provider()) { echo \Rooster\Admin\Ui::notice(esc_html__('Gravity Forms not active — IDs still save.', 'ai-eshop-optimizer'), 'warning', array('inline' => true)); } echo AIEO_DMM_UI::card_form('aieo_user_vitals_save_gf', 'user_vitals_gf', $aieo_uv_form, '
' . $aieo_uv_rows . '
'); echo $aieo_uv_close; } public function render_subtab() { global $wpdb; $tbl = $this->table(); $exists = (bool) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl)); $rows = $exists ? (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl}") : 0; $last = $exists ? $wpdb->get_var("SELECT MAX(migrated_at) FROM {$tbl}") : null; $src_ok = $this->src_exists(); $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION); $ajax = admin_url('admin-ajax.php'); if (function_exists('rooster_admin_band_open')) { rooster_admin_band_open(array('cols' => 2)); } $aieo_uv_fields = array(); foreach (self::fields() as $aieo_uv_col => $aieo_uv_meta) { $aieo_uv_fields[] = array( 'column' => (string) $aieo_uv_col, 'field' => (string) $aieo_uv_meta[0], 'type' => (string) $aieo_uv_meta[1], ); } AIEO_Table::card(array( 'anchor' => 'user-vitals-fields', 'icon' => 'id-card', 'heading' => 'h2', 'title' => __('Fields', 'ai-eshop-optimizer'), 'description' => sprintf( __('The SOFT1 / ERP fields that extended the WordPress user (customer id, loyalty ids + points, turnover, transaction dates, speciality, locations) now live in their own typed, indexed table %s. It replaces the legacy Pods user extension as the source for correlating user + ERP statistics with the order & product analytics (wired in a later stage, the same way the daily order/product vitals are).', 'ai-eshop-optimizer'), $wpdb->prefix . self::TABLE ), 'col' => 1, 'page_id' => $this->get_id(), 'row_key' => 'column', 'rows' => $aieo_uv_fields, 'columns' => array( array('key' => 'field', 'label' => __('Field', 'ai-eshop-optimizer')), array('key' => 'column', 'label' => __('Column', 'ai-eshop-optimizer'), 'type' => 'code'), array('key' => 'type', 'label' => __('Type', 'ai-eshop-optimizer'), 'type' => 'code'), ), )); \Rooster\Admin\States::enqueue(); $aieo_uv_last = $last ? ' · ' . esc_html__('last migrated', 'ai-eshop-optimizer') . ' ' . esc_html(get_date_from_gmt($last, 'Y-m-d H:i')) : ''; $aieo_uv_reason = __('The legacy wp_pods_user table was not found on this site — there is nothing to migrate here.', 'ai-eshop-optimizer'); $aieo_uv_scan = \Rooster\Admin\Ui::button(__('Scan (dry-run)', 'ai-eshop-optimizer'), array( 'attrs' => array('id' => 'aieo-uv-scan') + (!$src_ok ? array('aria-describedby' => 'aieo-uv-migrate-reason') : array()), 'disabled' => !$src_ok, )); $aieo_uv_job = \Rooster\Admin\States::job(array( 'id' => 'aieo-uv-job', 'state' => $last ? 'idle' : 'not-run', 'run' => __('Migrate now', 'ai-eshop-optimizer'), 'run_attrs' => array('id' => 'aieo-uv-migrate'), 'confirm' => 'aieo-uv-ask', 'disabled' => !$src_ok, 'reason' => !$src_ok ? $aieo_uv_reason : '', )); $aieo_uv_ask = \Rooster\Admin\States::confirm( 'aieo-uv-ask', 'replace', __('Migrate the SOFT1 / ERP user fields from Pods into the vitals table?', 'ai-eshop-optimizer'), esc_html(sprintf( _n('%s row is already in the vitals table. Migrating overwrites the rows of the users it finds in Pods, and never duplicates one.', '%s rows are already in the vitals table. Migrating overwrites the rows of the users it finds in Pods, and never duplicates one.', $rows, 'ai-eshop-optimizer'), number_format_i18n($rows) )), array('confirm' => __('Migrate', 'ai-eshop-optimizer')) ); $aieo_uv_notes = array( 'ok' => \Rooster\Admin\Ui::notice('{text}', 'info', array('inline' => true)), 'fail' => \Rooster\Admin\Ui::notice('{text}', 'destructive', array('inline' => true)), ); list($aieo_uv_open, $aieo_uv_close) = AIEO_DMM_UI::card(array( 'anchor' => 'user-vitals-migrate-from-the-pods-user-extension', 'icon' => 'import', 'title' => __('Migrate ERP fields from Pods', 'ai-eshop-optimizer'), 'description' => $src_ok ? __('Reads the legacy wp_pods_user table and writes one typed row per user into the vitals table. Idempotent — re-running overwrites each user’s row (keyed on user id), never duplicates. Users with no matching WP account are skipped. Run Scan first to preview the counts.', 'ai-eshop-optimizer') : __('The legacy wp_pods_user table was not found on this site — there is nothing to migrate here.', 'ai-eshop-optimizer'), 'footer' => '

' . esc_html__('Vitals table:', 'ai-eshop-optimizer') . ' ' . '' . esc_html(number_format_i18n($rows)) . ' ' . esc_html__('rows', 'ai-eshop-optimizer') . $aieo_uv_last . '

', 'col' => 2, )); echo $aieo_uv_open; echo $aieo_uv_scan; ?> 2)); } $this->render_loyalty_card(); $this->render_gf_card(); if (function_exists('rooster_admin_band_close')) { rooster_admin_band_close(); } } }