api_client = $api_client; $this->init_hooks(); } /** * Initialize WordPress hooks */ private function init_hooks() { // Add settings tab to WooCommerce. add_filter('woocommerce_settings_tabs_array', array($this, 'add_settings_tab'), 50); add_action('woocommerce_settings_tabs_activetrail', array($this, 'settings_tab_content')); add_action('woocommerce_update_options_activetrail', array($this, 'update_settings')); // Initialize custom field types immediately. $this->init_custom_field_types(); // Enqueue admin scripts and styles. add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); // Add admin notices. add_action('admin_notices', array($this, 'admin_notices')); // Add plugin meta links. add_filter('plugin_row_meta', array($this, 'plugin_row_meta'), 10, 2); // Add AJAX handlers. add_action('wp_ajax_activetrail_save_group', array($this, 'ajax_save_group')); add_action('wp_ajax_activetrail_save_api_key', array($this, 'ajax_save_api_key')); add_action('wp_ajax_activetrail_connect_store', array($this, 'ajax_connect_store')); add_action('wp_ajax_activetrail_get_debug_info', array($this, 'ajax_get_debug_info')); add_action('wp_ajax_activetrail_test_api', array($this, 'ajax_test_api')); add_action('wp_ajax_activetrail_get_groups', array($this, 'ajax_get_groups')); add_action('wp_ajax_activetrail_delete_coupon', array($this, 'ajax_delete_coupon')); } /** * Add settings tab to WooCommerce * * @param array $settings_tabs Existing tabs. * @return array Modified tabs */ public function add_settings_tab($settings_tabs) { $settings_tabs['activetrail'] = esc_html__('ActiveTrail', 'activetrail-for-woocommerce'); return $settings_tabs; } /** * Settings tab content */ public function settings_tab_content() { // Switch to plugin's language setting and reload text domain for translations. $this->switch_to_plugin_language(); // Add sub-navigation tabs. $this->output_sub_navigation(); // Split settings into three buckets: before Group, Group section, and everything after. $pre_group = array(); $group_section = array(); $post_group = array(); $in_group = false; $group_done = false; foreach ( $this->get_settings() as $setting ) { $id = isset( $setting['id'] ) ? $setting['id'] : ''; $type = isset( $setting['type'] ) ? $setting['type'] : ''; if ( 'title' === $type && 'activetrail_group_settings' === $id ) { $in_group = true; $group_section[] = $setting; } elseif ( 'sectionend' === $type && 'activetrail_group_settings' === $id ) { $group_section[] = $setting; $in_group = false; $group_done = true; } elseif ( $in_group ) { $group_section[] = $setting; } elseif ( $group_done ) { $post_group[] = $setting; } else { $pre_group[] = $setting; } } // Render pre-Group sections (Language, API, etc.). woocommerce_admin_fields( $pre_group ); // Render Group section wrapped in a div. // Lock the section if no API key is saved yet. $api_key_saved = get_option( 'activetrail_api_key', '' ); $group_lock_class = empty( $api_key_saved ) ? ' at-group-locked' : ''; echo '
'; woocommerce_admin_fields( $group_section ); echo '
'; // Render remaining sections (Order Sync, Checkout, etc.). woocommerce_admin_fields( $post_group ); } /** * Get guide URL based on current language * * @return string Guide URL */ private function get_guide_url() { require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/settings/class-settings.php'; $current_language = ActiveTrail_Settings::get_current_language(); $guide_urls = array( 'he_IL' => 'https://www.activetrail.co.il/integrations_il/learn/guides_il/%D7%90%D7%A7%D7%98%D7%99%D7%91-%D7%98%D7%A8%D7%99%D7%99%D7%9C-%D7%A2%D7%91%D7%95%D7%A8-woocommerce/', 'en_US' => 'https://www.activetrail.com/integrations/learning/guides/integrate_woocommerce_with_activetrail/', 'fr_FR' => 'https://www.activetrail.fr/integrations_fr/learning_center/guides_fr/integrate_woocommerce_avec_activetrail/?_gl=1*1t353zn*_gcl_au*NDYyOTIxNzgwLjE3NjE1NTU0NTEuNjY5MTQ2MTM0LjE3NjQ2MDQzMTguMTc2NDYwNDM0MA..', 'es_ES' => 'https://www.activetrail.es/integrations_es/aprender/guides_es/integra-activetrail-con-woocommerce/?_gl=1*1t353zn*_gcl_au*NDYyOTIxNzgwLjE3NjE1NTU0NTEuNjY5MTQ2MTM0LjE3NjQ2MDQzMTguMTc2NDYwNDM0MA..' ); // Default to English if language not found. return isset($guide_urls[$current_language]) ? $guide_urls[$current_language] : $guide_urls['en_US']; } /** * Output sub-navigation tabs */ private function output_sub_navigation() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation only, no data processing $current_section = isset($_GET['section']) ? sanitize_text_field(wp_unslash($_GET['section'])) : ''; $tabs = array( '' => array( 'label' => esc_html__('General Settings', 'activetrail-for-woocommerce'), 'icon' => 'dashicons-admin-settings', 'url' => admin_url('admin.php?page=wc-settings&tab=activetrail'), 'external' => false ), 'export' => array( 'label' => esc_html__('Product Export', 'activetrail-for-woocommerce'), 'icon' => 'dashicons-download', 'url' => admin_url('admin.php?page=wc-settings&tab=activetrail§ion=export'), 'external' => false ), 'guide' => array( 'label' => esc_html__('Full Guide', 'activetrail-for-woocommerce'), 'icon' => 'dashicons-book', 'url' => $this->get_guide_url(), 'external' => true, 'never_active' => true ) ); ?>
get_settings()); // Additional processing after save. $this->process_settings_update(); // Check if we should create a coupon from settings. $this->maybe_create_coupon_from_settings(); } /** * Get settings fields * * @return array */ public function get_settings() { // Switch to plugin's language setting and reload text domain for translations. // This must be done BEFORE creating the settings array so translations are applied. $this->switch_to_plugin_language(); // Check if we're on the export tab. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation only, no data processing $current_tab = isset($_GET['section']) ? sanitize_text_field(wp_unslash($_GET['section'])) : ''; if ( 'export' === $current_tab ) { return $this->get_export_settings(); } // Get current settings from our unified settings system. $current_settings = ActiveTrail_Settings::get_all(); $settings = array( array( 'name' => esc_html__('ActiveTrail Integration Settings', 'activetrail-for-woocommerce'), 'type' => 'title', 'desc' => esc_html__('Configure your ActiveTrail integration settings below.', 'activetrail-for-woocommerce'), 'id' => 'activetrail_settings' ), // Close the table opened by the global title above so subsequent sections // are at the top level and our wrapper
injections produce valid HTML. array( 'type' => 'sectionend', 'id' => 'activetrail_settings', ), // Language Settings Section. array( 'name' => esc_html__('Language Settings', 'activetrail-for-woocommerce'), 'type' => 'title', 'id' => 'activetrail_language_settings' ), array( 'name' => esc_html__('Plugin Language', 'activetrail-for-woocommerce'), 'type' => 'select', 'desc' => esc_html__('Select the language for the ActiveTrail plugin interface. After selecting your preferred language, click the blue "Save Changes" button at the bottom of the page. Then refresh your browser for the changes to take effect properly.', 'activetrail-for-woocommerce'), 'id' => 'activetrail_language', 'options' => ActiveTrail_Settings::get_available_languages(), 'default' => $current_settings['language'], 'css' => 'min-width:200px;', 'class' => 'activetrail-language-select' ), array( 'type' => 'sectionend', 'id' => 'activetrail_language_settings' ), // API Configuration Section. array( 'name' => esc_html__('API Configuration', 'activetrail-for-woocommerce'), 'type' => 'title', 'id' => 'activetrail_api_settings' ), array( 'name' => esc_html__('API Key', 'activetrail-for-woocommerce'), 'type' => 'activetrail_api_key', 'desc' => esc_html__('Enter your ActiveTrail API key. You can find this in your ActiveTrail account settings.', 'activetrail-for-woocommerce'), 'id' => 'activetrail_api_key', 'css' => 'min-width:300px;', 'default' => $current_settings['api_key'], 'custom_attributes' => array( 'autocomplete' => 'off', 'required' => 'required' ), 'required' => true ), array( 'type' => 'sectionend', 'id' => 'activetrail_api_settings' ), // Group Section. array( 'name' => esc_html__('Group', 'activetrail-for-woocommerce'), 'type' => 'title', 'id' => 'activetrail_group_settings' ), array( 'name' => esc_html__('Group ID', 'activetrail-for-woocommerce'), 'type' => 'activetrail_group_select', 'desc' => esc_html__('Select the ActiveTrail group where customers will be added.', 'activetrail-for-woocommerce'), 'id' => 'activetrail_group_id', 'options' => $this->get_groups_options(), 'default' => $current_settings['group_id'], 'css' => 'min-width:300px;', 'required' => true ), array( 'type' => 'sectionend', 'id' => 'activetrail_group_settings' ), // Order Sync Section. array( 'name' => esc_html__('Order Synchronization', 'activetrail-for-woocommerce'), 'type' => 'title', 'id' => 'activetrail_order_settings' ), array( 'name' => esc_html__('Synchronized Statuses', 'activetrail-for-woocommerce'), 'type' => 'activetrail_status_table', 'desc' => esc_html__('Select which order statuses should trigger events to ActiveTrail. If no statuses are selected, all statuses will be synchronized (backward compatible).', 'activetrail-for-woocommerce'), 'id' => 'activetrail_status_mapping', 'default' => ActiveTrail_Settings::get_status_mapping(), ), array( 'type' => 'sectionend', 'id' => 'activetrail_order_settings' ), // Checkout Section. array( 'name' => esc_html__('Checkout Settings', 'activetrail-for-woocommerce'), 'type' => 'title', 'id' => 'activetrail_checkout_settings' ), array( 'name' => esc_html__('Enable Consent Checkbox', 'activetrail-for-woocommerce'), 'type' => 'checkbox', 'desc' => esc_html__('Add a marketing consent checkbox to the checkout form', 'activetrail-for-woocommerce'), 'id' => 'activetrail_enable_checkout_consent', 'default' => $current_settings['enable_checkout_consent'] ? 'yes' : 'no' ), array( 'name' => esc_html__('Enable Registration Consent', 'activetrail-for-woocommerce'), 'type' => 'checkbox', 'desc' => esc_html__('Add a marketing consent checkbox to the user registration form', 'activetrail-for-woocommerce'), 'id' => 'activetrail_enable_registration_consent', 'default' => $current_settings['enable_registration_consent'] ? 'yes' : 'no' ), array( 'name' => esc_html__('Consent Text', 'activetrail-for-woocommerce'), 'type' => 'textarea', 'desc' => esc_html__('Text displayed next to the consent checkbox.', 'activetrail-for-woocommerce'), 'id' => 'activetrail_consent_text', 'default' => $current_settings['consent_text'], 'css' => 'min-width:300px; height:60px;' ), // If enabled, the marketing consent checkbox on frontend is checked by default. array( 'name' => esc_html__('Marketing consent checked by default', 'activetrail-for-woocommerce'), 'type' => 'checkbox', 'desc' => esc_html__('When enabled, the marketing consent checkbox is checked by default on checkout and registration.', 'activetrail-for-woocommerce'), 'id' => 'activetrail_consent_checked_by_default', 'default' => ! empty( $current_settings['consent_checked_by_default'] ) ? 'yes' : 'no' ), array( 'type' => 'sectionend', 'id' => 'activetrail_checkout_settings' ), // Cart Tracking Section. array( 'name' => esc_html__('Cart Tracking', 'activetrail-for-woocommerce'), 'type' => 'title', 'id' => 'activetrail_cart_settings' ), array( 'name' => esc_html__('Enable Cart Tracking', 'activetrail-for-woocommerce'), 'type' => 'checkbox', 'desc' => esc_html__('Track cart changes and send data to ActiveTrail', 'activetrail-for-woocommerce'), 'id' => 'activetrail_enable_cart_tracking', 'default' => $current_settings['enable_cart_tracking'] ? 'yes' : 'no' ), array( 'type' => 'sectionend', 'id' => 'activetrail_cart_settings' ), // Product Feed Section. array( 'name' => esc_html__('Product Feed', 'activetrail-for-woocommerce'), 'type' => 'title', 'id' => 'activetrail_feed_settings' ), array( 'name' => esc_html__('Feed Title', 'activetrail-for-woocommerce'), 'type' => 'text', 'desc' => esc_html__( 'Product feed title.', 'activetrail-for-woocommerce' ), 'id' => 'activetrail_feed_title', 'default' => $current_settings['feed_title'], 'css' => 'min-width:300px;' ), array( 'name' => esc_html__('Feed Description', 'activetrail-for-woocommerce'), 'type' => 'textarea', 'desc' => esc_html__( 'Product feed description.', 'activetrail-for-woocommerce' ), 'id' => 'activetrail_feed_description', 'default' => $current_settings['feed_description'], 'css' => 'min-width:300px; height:60px;' ), array( 'type' => 'sectionend', 'id' => 'activetrail_feed_settings' ), // Historical Sync Section. array( 'name' => esc_html__('Historical Contacts and Orders Sync', 'activetrail-for-woocommerce'), 'type' => 'title', 'desc' => esc_html__('Sync existing customers from your WooCommerce store to ActiveTrail. This is useful when connecting to a new ActiveTrail account or when you need to sync historical data.', 'activetrail-for-woocommerce'), 'id' => 'activetrail_historical_sync_settings' ), array( 'name' => '', 'type' => 'activetrail_historical_sync', 'id' => 'activetrail_historical_sync', ), array( 'type' => 'sectionend', 'id' => 'activetrail_historical_sync_settings' ), // Coupon Management Section - HIDDEN FROM UI. // array(. // 'name' => esc_html__('Coupon Management', 'activetrail-for-woocommerce'),. // 'type' => 'title',. // 'desc' => esc_html__('Create and manage WooCommerce coupons directly from the ActiveTrail plugin.', 'activetrail-for-woocommerce'),. // 'id' => 'activetrail_coupon_settings'. // ),. // array(. // 'name' => '',. // 'type' => 'activetrail_coupon_manager',. // 'id' => 'activetrail_coupon_manager',. // ),. // array(. // 'type' => 'sectionend',. // 'id' => 'activetrail_coupon_settings'. // ),. ); return apply_filters('activetrail_settings_fields', $settings); } /** * Get export settings fields * * @return array */ public function get_export_settings() { // Switch to plugin's language setting and reload text domain for translations. // This must be done BEFORE creating the settings array so translations are applied. $this->switch_to_plugin_language(); // Generate secure keys if they don't exist. $google_key = get_option('activetrail_google_feed_key', ''); $facebook_key = get_option('activetrail_facebook_feed_key', ''); if (empty($google_key)) { $google_key = $this->generate_secure_feed_key(); update_option('activetrail_google_feed_key', $google_key); } if (empty($facebook_key)) { $facebook_key = $this->generate_secure_feed_key(); update_option('activetrail_facebook_feed_key', $facebook_key); } // Get product count. $product_count = wp_count_posts('product'); $total_products = isset($product_count->publish) ? (int) $product_count->publish : 0; // Get last feed update time for display. $last_update = get_option('activetrail_feed_last_update', ''); if (!empty($last_update)) { $timestamp = strtotime($last_update); $last_update_display = $timestamp ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $timestamp) : $last_update; } else { $last_update_display = esc_html__('Feed has not been generated yet.', 'activetrail-for-woocommerce'); } $settings = array( array( 'name' => esc_html__('Product Catalog Export', 'activetrail-for-woocommerce'), 'type' => 'title', 'desc' => esc_html__('Export your product catalog for Google Merchant Center and Facebook Catalog. The feeds update automatically when products change.', 'activetrail-for-woocommerce'), 'id' => 'activetrail_export_settings' ), array( 'name' => '', 'type' => 'activetrail_export_feeds', 'id' => 'activetrail_export_feeds', 'google_url' => home_url('?activetrail_export=google&key=' . $google_key), 'facebook_url' => home_url('?activetrail_export=facebook&key=' . $facebook_key), 'product_count' => $total_products, 'last_update' => $last_update_display, 'google_key' => $google_key, 'facebook_key' => $facebook_key ), array( 'type' => 'sectionend', 'id' => 'activetrail_export_settings' ), ); return $settings; } /** * Generate secure feed key * * @return string */ private function generate_secure_feed_key() { return wp_generate_password(32, false, false); } /** * Get groups options for select field * * @return array */ private function get_groups_options() { // Get current settings to check if API key is available. $current_settings = ActiveTrail_Settings::get_all(); // If no API key, return default option only. if (empty($current_settings['api_key'])) { return array('' => esc_html__('Enter API key first to load groups', 'activetrail-for-woocommerce')); } // Try to get groups from settings. $groups_options = ActiveTrail_Settings::get_groups_for_select(); // If we only have the default option, try to force refresh the groups. if (count($groups_options) <= 1) { // Force refresh groups from API. $groups_result = ActiveTrail_Settings::get_activetrail_groups(true); if ($groups_result['success'] && !empty($groups_result['data'])) { // Manually build the options array. $groups_options = array('' => esc_html__('Select a group...', 'activetrail-for-woocommerce')); foreach ($groups_result['data'] as $group) { $id = isset($group['Id']) ? $group['Id'] : (isset($group['id']) ? $group['id'] : ''); $name = isset($group['Name']) ? $group['Name'] : (isset($group['name']) ? $group['name'] : ''); if (!empty($id) && !empty($name)) { $groups_options[$id] = $name; } } } else { // API call failed, show error message. $error_message = isset($groups_result['message']) ? $groups_result['message'] : esc_html__('Failed to load groups', 'activetrail-for-woocommerce'); return array('' => $error_message); } } // Always append "Load more" as the last option so it is visible even before JS runs. $groups_options['__load_more__'] = esc_html__( '── Load more... ──', 'activetrail-for-woocommerce' ); return $groups_options; } /** * Get order statuses options * * @return array */ private function get_order_statuses_options() { $statuses = wc_get_order_statuses(); $options = array(); foreach ($statuses as $key => $label) { $clean_key = str_replace('wc-', '', $key); $options[$clean_key] = $label; } return $options; } /** * Process settings update */ private function process_settings_update() { // Verify nonce for security. if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) { return; } // Get current settings first to preserve any existing data. $current_settings = ActiveTrail_Settings::get_all(); // Get selected statuses from checkboxes. $detailed_statuses = ActiveTrail_Settings::get_order_statuses_detailed(); $status_mapping = array(); $enabled_statuses = array(); // Process status checkboxes from POST data. foreach ($detailed_statuses as $status_key => $status_info) { // Get description from POST if available, otherwise use default. $description = $status_info['description']; if (isset($_POST['activetrail_status_mapping'][$status_key]['description'])) { $description = sanitize_text_field(wp_unslash($_POST['activetrail_status_mapping'][$status_key]['description'])); } // Check if checkbox is checked. $is_enabled = isset($_POST['activetrail_status_mapping'][$status_key]['sync']) && sanitize_text_field(wp_unslash($_POST['activetrail_status_mapping'][$status_key]['sync'])) === '1'; $status_mapping[$status_key] = array( 'sync' => $is_enabled, 'description' => $description ); if ($is_enabled) { $enabled_statuses[] = $status_key; } } // Save the status mapping - all statuses always enabled. ActiveTrail_Settings::update_status_mapping($status_mapping); // Validate required fields. $api_key = get_option('activetrail_api_key', ''); $group_id = get_option('activetrail_group_id', ''); if (empty($api_key)) { WC_Admin_Settings::add_error(esc_html__('API Key is required. Please enter your ActiveTrail API key.', 'activetrail-for-woocommerce')); } if (empty($group_id)) { WC_Admin_Settings::add_error(esc_html__('Group ID is required. Please select a group.', 'activetrail-for-woocommerce')); } // Update plugin settings using our settings class. $new_settings = array( 'api_key' => $api_key, 'group_id' => $group_id, 'order_statuses' => $enabled_statuses, 'enable_checkout_consent' => get_option('activetrail_enable_checkout_consent', 'yes') === 'yes', 'consent_text' => get_option('activetrail_consent_text', ''), 'consent_checked_by_default' => get_option('activetrail_consent_checked_by_default', 'no') === 'yes', 'consent_required' => false, 'enable_registration_consent' => get_option('activetrail_enable_registration_consent', 'no') === 'yes', 'sync_without_consent' => get_option('activetrail_sync_without_consent', 'no') === 'yes', 'enable_cart_tracking' => get_option('activetrail_enable_cart_tracking', 'yes') === 'yes', 'enable_product_feed' => true, // Always enabled 'feed_title' => get_option('activetrail_feed_title', ''), 'feed_description' => get_option('activetrail_feed_description', ''), 'debug_mode' => get_option('activetrail_debug_mode', 'no') === 'yes', 'language' => get_option('activetrail_language', 'en_US'), ); // Merge with current settings to preserve cache and other data. $merged_settings = array_merge($current_settings, $new_settings); // Update unified settings. $update_result = ActiveTrail_Settings::update_all($merged_settings); // Debug logging if enabled. if ($merged_settings['debug_mode'] && function_exists('activetrail_logger')) { $logger = activetrail_logger(); $logger->debug('Settings update - Group ID: ' . ($merged_settings['group_id'] ? $merged_settings['group_id'] : 'empty'), 'SETTINGS'); $logger->debug('Settings update - Enabled statuses: ' . implode(', ', $enabled_statuses), 'SETTINGS'); $logger->debug('Settings update result: ' . ($update_result ? 'success' : 'failed'), 'SETTINGS'); } // Clear groups cache if group_id changed. if ($current_settings['group_id'] !== $merged_settings['group_id']) { ActiveTrail_Settings::clear_groups_cache(); } // Flush rewrite rules if feed settings changed. if ($merged_settings['enable_product_feed']) { flush_rewrite_rules(); } // Clean up temporary WooCommerce options. delete_option('activetrail_status_mapping'); } /** * Enqueue admin scripts and styles * * @param string $hook Current admin page hook. */ public function enqueue_admin_scripts($hook) { // Debug logging. if (function_exists('activetrail_logger')) { $logger = activetrail_logger(); $logger->debug('enqueue_admin_scripts called with hook: ' . $hook, 'GENERAL'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Debug logging only $get_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'not set'; $logger->debug('GET tab: ' . $get_tab, 'GENERAL'); } // Only load on WooCommerce settings page. if ( 'woocommerce_page_wc-settings' !== $hook ) { if (function_exists('activetrail_logger')) { activetrail_logger()->debug('Not WooCommerce settings page, returning', 'GENERAL'); } return; } // Only load on our tab. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Debug logging only $get_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : ''; if ( 'activetrail' !== $get_tab ) { if (function_exists('activetrail_logger')) { activetrail_logger()->debug('Not ActiveTrail tab, returning', 'GENERAL'); } return; } if (function_exists('activetrail_logger')) { activetrail_logger()->debug('Loading admin scripts...', 'GENERAL'); } wp_enqueue_script( 'activetrail-admin', ACTIVETRAIL_WC_PLUGIN_URL . 'assets/js/admin-script.js', array('jquery', 'jquery-ui-tooltip'), ACTIVETRAIL_WC_VERSION, true ); wp_enqueue_style( 'activetrail-admin', ACTIVETRAIL_WC_PLUGIN_URL . 'assets/css/admin-style.css', array(), ACTIVETRAIL_WC_VERSION ); // Get current section to conditionally add inline styles/scripts. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation only, no data processing $current_section = isset($_GET['section']) ? sanitize_text_field(wp_unslash($_GET['section'])) : ''; // Add inline styles for sub-navigation (always needed). $sub_nav_css = $this->get_sub_nav_css(); wp_add_inline_style('activetrail-admin', $sub_nav_css); // Add inline styles and scripts based on section. if ($current_section === 'export') { $export_css = $this->get_export_css(); $export_js = $this->get_export_js(); wp_add_inline_style('activetrail-admin', $export_css); wp_add_inline_script('activetrail-admin', $export_js); } elseif ($current_section === 'coupons') { $coupon_css = $this->get_coupon_css(); $coupon_js = $this->get_coupon_js(); wp_add_inline_style('activetrail-admin', $coupon_css); wp_add_inline_script('activetrail-admin', $coupon_js); } elseif ($current_section === 'sync') { $status_css = $this->get_status_css(); wp_add_inline_style('activetrail-admin', $status_css); } // Always add historical sync JS/CSS (needed on main settings page where the field is displayed) // This ensures the buttons work even when not on the historical-sync section $historical_css = $this->get_historical_sync_css(); $historical_js = $this->get_historical_sync_js(); wp_add_inline_style('activetrail-admin', $historical_css); wp_add_inline_script('activetrail-admin', $historical_js); // Add required field indicators script (always needed on settings page). $required_field_js = $this->get_required_field_indicators_js(); wp_add_inline_script('activetrail-admin', $required_field_js); // Add API key and group select scripts (always needed on settings page). $api_key_js = $this->get_api_key_field_js(); $group_select_js = $this->get_group_select_field_js(); wp_add_inline_script('activetrail-admin', $api_key_js); wp_add_inline_script('activetrail-admin', $group_select_js); // Localize script. wp_localize_script('activetrail-admin', 'activetrail_admin', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('activetrail_admin_nonce'), 'strings' => array( 'testing_connection' => esc_html__('Testing connection...', 'activetrail-for-woocommerce'), 'connection_successful' => esc_html__('Connection successful!', 'activetrail-for-woocommerce'), 'connection_failed' => esc_html__('Connection failed:', 'activetrail-for-woocommerce'), 'loading_groups' => esc_html__('Loading groups...', 'activetrail-for-woocommerce'), 'groups_loaded' => esc_html__('Groups loaded successfully', 'activetrail-for-woocommerce'), 'groups_failed' => esc_html__('Failed to load groups:', 'activetrail-for-woocommerce'), 'refresh_groups' => esc_html__('Refresh Groups', 'activetrail-for-woocommerce'), 'copy' => esc_html__('Copy', 'activetrail-for-woocommerce'), 'product_feed_urls' => esc_html__('Product Feed URLs:', 'activetrail-for-woocommerce'), 'xml_feed' => esc_html__('XML Feed:', 'activetrail-for-woocommerce'), 'json_feed' => esc_html__('JSON Feed:', 'activetrail-for-woocommerce'), 'use_these_urls' => esc_html__('Use these URLs in your ActiveTrail campaigns or external integrations.', 'activetrail-for-woocommerce'), 'settings_saved' => esc_html__('Settings saved successfully', 'activetrail-for-woocommerce'), 'error_saving' => esc_html__('Error saving settings', 'activetrail-for-woocommerce'), 'plugin_language_change_hebrew' => esc_html__('Plugin language will change to Hebrew after saving settings', 'activetrail-for-woocommerce'), 'plugin_language_change_spanish' => esc_html__('Plugin language will change to Spanish after saving settings', 'activetrail-for-woocommerce'), 'plugin_language_change_french' => esc_html__('Plugin language will change to French after saving settings', 'activetrail-for-woocommerce'), 'plugin_language_change_english' => esc_html__('Plugin language will change to English after saving settings', 'activetrail-for-woocommerce'), 'saving_and_switching' => esc_html__('Saving settings and switching language', 'activetrail-for-woocommerce'), 'saving' => esc_html__('Saving...', 'activetrail-for-woocommerce'), // Additional button translations for different languages. 'refresh_groups_hebrew' => esc_html__('רענן קבוצות', 'activetrail-for-woocommerce'), 'refresh_groups_spanish' => esc_html__('Actualizar grupos', 'activetrail-for-woocommerce'), 'refresh_groups_french' => esc_html__('Actualiser les groupes', 'activetrail-for-woocommerce'), 'save_group_hebrew' => esc_html__('שמור קבוצה', 'activetrail-for-woocommerce'), 'save_group_spanish' => esc_html__('Guardar grupo', 'activetrail-for-woocommerce'), 'save_group_french' => esc_html__('Enregistrer le groupe', 'activetrail-for-woocommerce'), 'connect_store' => esc_html__('Connect Store', 'activetrail-for-woocommerce'), 'connect_store_hebrew' => esc_html__('חבר חנות', 'activetrail-for-woocommerce'), 'connect_store_spanish' => esc_html__('Conectar tienda', 'activetrail-for-woocommerce'), 'connect_store_french' => esc_html__('Connecter la boutique', 'activetrail-for-woocommerce'), 'connecting_store' => esc_html__('Connecting...', 'activetrail-for-woocommerce'), 'store_connected' => esc_html__('Store connected successfully!', 'activetrail-for-woocommerce'), 'store_connected_hebrew' => 'החנות מחוברת בהצלחה!', 'store_connected_spanish' => '¡Tienda conectada exitosamente!', 'store_connected_french' => 'Boutique connectée avec succès !', ), 'store_is_connected' => ( get_option('activetrail_store_status') === 'connected' ), )); } /** * Get sub-navigation CSS * * @return string CSS content */ private function get_sub_nav_css() { return '.activetrail-sub-nav-wrapper { margin: 0 0 30px 0; background: linear-gradient(135deg, #262e3e 0%, #262e3e 100%); border-radius: 12px; box-shadow: 0 8px 32px rgba(38, 46, 62, 0.2); overflow: hidden; position: relative; } .activetrail-sub-nav-wrapper::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%); pointer-events: none; } .activetrail-sub-nav { display: flex; align-items: center; gap: 24px; margin: 0; padding: 24px 40px 24px 40px; position: relative; z-index: 1; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; } .activetrail-sub-nav::-webkit-scrollbar { display: none; } .activetrail-sub-nav-item { display: flex; align-items: center; justify-content: center; gap: 10px; padding: 14px 24px; text-decoration: none; color: rgba(255, 255, 255, 0.8); border-radius: 8px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-weight: 600; font-size: 14px; white-space: nowrap; position: relative; backdrop-filter: blur(10px); min-width: fit-content; flex-shrink: 0; } .activetrail-sub-nav-item::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.1); border-radius: 8px; opacity: 0; transition: opacity 0.3s ease; } .activetrail-sub-nav-item:hover { color: #fff; text-decoration: none; transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); } .activetrail-sub-nav-item:hover::before { opacity: 1; } .activetrail-sub-nav-item.active { color: #fff; background: #ef4b66; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); transform: translateY(-1px); } .activetrail-sub-nav-item:focus { outline: 2px solid rgba(255, 255, 255, 0.6); outline-offset: 2px; } .activetrail-sub-nav-item.active:focus { outline: 2px solid rgba(255, 255, 255, 0.8); outline-offset: 2px; } .activetrail-sub-nav-item[href*="activetrail.com"] { transition: none !important; transform: none !important; box-shadow: none !important; background: rgba(255, 255, 255, 0.1) !important; color: rgba(255, 255, 255, 0.8) !important; } .activetrail-sub-nav-item[href*="activetrail.com"]:hover { color: rgba(255, 255, 255, 0.8) !important; transform: none !important; box-shadow: none !important; background: rgba(255, 255, 255, 0.1) !important; } .activetrail-sub-nav-item[href*="activetrail.com"]:focus { outline: none !important; background: rgba(255, 255, 255, 0.1) !important; color: rgba(255, 255, 255, 0.8) !important; } .activetrail-sub-nav-item[href*="activetrail.com"]:active { background: rgba(255, 255, 255, 0.1) !important; color: rgba(255, 255, 255, 0.8) !important; transform: none !important; } .activetrail-sub-nav-item[href*="activetrail.com"]::before { display: none !important; } .activetrail-sub-nav-item[href*="activetrail.com"]:hover::before { display: none !important; } .activetrail-sub-nav-item:not([href*="activetrail.com"]) { color: rgba(255, 255, 255, 0.8) !important; } .activetrail-sub-nav-item:not([href*="activetrail.com"]):hover { color: #fff !important; } .activetrail-sub-nav-item:not([href*="activetrail.com"]):focus { color: #fff !important; } .activetrail-sub-nav-item:not([href*="activetrail.com"]):active { color: #fff !important; } .activetrail-sub-nav-item.active { color: #fff !important; } .activetrail-sub-nav-item.active:hover { color: #fff !important; } .activetrail-sub-nav-item.active:focus { color: #fff !important; } .activetrail-sub-nav-item.active:active { color: #fff !important; } .activetrail-sub-nav-item .dashicons { font-size: 18px; width: 18px; height: 18px; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); } .activetrail-logo-container { margin-right: auto; display: flex; align-items: center; justify-content: center; padding: 0 20px; } .activetrail-logo-link { display: inline-block; text-decoration: none; border-radius: 6px; padding: 4px; transition: all 0.3s ease; } .activetrail-logo-link:hover { background-color: rgba(255, 255, 255, 0.1); transform: scale(1.05); } .activetrail-logo-link:focus { outline: 2px solid rgba(255, 255, 255, 0.5); outline-offset: 2px; } .activetrail-logo { height: 48px; width: auto; opacity: 0.9; transition: opacity 0.3s ease, transform 0.3s ease; display: block; } .activetrail-logo:hover { opacity: 1; } @media (max-width: 1024px) { .activetrail-sub-nav-item { padding: 12px 20px; font-size: 13px; align-items: center; justify-content: center; } .activetrail-sub-nav-item .dashicons { font-size: 16px; width: 16px; height: 16px; } .activetrail-logo { height: 42px; } } @media (max-width: 768px) { .activetrail-sub-nav-wrapper { margin: 0 0 25px 0; border-radius: 8px; } .activetrail-sub-nav { padding: 16px 24px 16px 24px; gap: 16px; align-items: center; } .activetrail-sub-nav-item { padding: 10px 16px; font-size: 12px; } .activetrail-sub-nav-item .dashicons { font-size: 14px; width: 14px; height: 14px; } .activetrail-logo { height: 36px; } .activetrail-logo-container { padding: 0 10px; } }'; } /** * Get status sync CSS * * @return string CSS content */ private function get_status_css() { return '.activetrail-status-table { border-collapse: collapse; table-layout: fixed; width: 100%; } .activetrail-status-table th, .activetrail-status-table td { padding: 12px 15px; border: 1px solid #ddd; vertical-align: middle; word-wrap: break-word; } .activetrail-status-table th { background: #f1f1f1; font-weight: 600; text-align: left; } .activetrail-status-table th:first-child { width: 120px; } .activetrail-status-table th:nth-child(2) { width: 80px; text-align: center; } .activetrail-status-table th:nth-child(3) { width: auto; } .activetrail-status-table th:nth-child(4) { width: 150px; text-align: left; } .activetrail-status-table .custom-status { background: #fff9e6; } .activetrail-status-table .sync-enabled { background: #e8f5e8; } .activetrail-category-header td { background: #f9f9f9 !important; font-weight: bold; border-top: 2px solid #0073aa; } .activetrail-status-table td { text-align: left; } .activetrail-status-table td:nth-child(2) { text-align: center; } .custom-badge { background: #ff9800; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px; margin-left: 5px; } .recommended-badge { background: #4caf50; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px; margin-left: 5px; } .category-badge { padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; } .category-pending { background: #fff3cd; color: #856404; } .category-active { background: #d1ecf1; color: #0c5460; } .category-completed { background: #d4edda; color: #155724; } .category-cancelled { background: #f8d7da; color: #721c24; } .category-custom { background: #e2e3e5; color: #383d41; } .summary-stats { display: flex; gap: 20px; margin-bottom: 10px; } .summary-stats .stat { color: #666; }'; } /** * Get export section CSS * * @return string CSS content */ private function get_export_css() { return '.feed-url-box { display: flex; gap: 10px; align-items: center; } .feed-url-input { flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; font-family: monospace; font-size: 13px; color: #333; } .copy-url-btn, .refresh-key-btn { white-space: nowrap; display: flex; align-items: center; gap: 5px; } .copy-url-btn.copied { background: #46b450; color: white; border-color: #46b450; } .refresh-key-notice { margin-top: 15px; padding: 12px; background: #f0f6fc; border-left: 4px solid #2271b1; color: #1e1e1e; font-size: 13px; line-height: 1.6; border-radius: 4px; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .refresh-key-btn .spin { animation: spin 1s linear infinite; } @media (max-width: 768px) { .feed-url-box { flex-direction: column; align-items: stretch; } .copy-url-btn, .refresh-key-btn { width: 100%; justify-content: center; } }'; } /** * Get export section JavaScript * * @return string JavaScript content */ private function get_export_js() { $base_url = esc_js( home_url() ); $ajax_url = esc_js( admin_url( 'admin-ajax.php' ) ); $nonce = esc_js( wp_create_nonce( 'activetrail_admin_nonce' ) ); $no_url = esc_js( esc_html__( 'No URL available.', 'activetrail-for-woocommerce' ) ); $copied = esc_js( esc_html__( 'Copied!', 'activetrail-for-woocommerce' ) ); $failed_copy = esc_js( esc_html__( 'Failed to copy. Please select and copy manually.', 'activetrail-for-woocommerce' ) ); $invalid_feed = esc_js( esc_html__( 'Invalid feed type.', 'activetrail-for-woocommerce' ) ); $key_refreshed = esc_js( esc_html__( 'Security key refreshed successfully!', 'activetrail-for-woocommerce' ) ); $failed_refresh = esc_js( esc_html__( 'Failed to refresh key:', 'activetrail-for-woocommerce' ) ); $unknown_error = esc_js( esc_html__( 'Unknown error', 'activetrail-for-woocommerce' ) ); $network_error = esc_js( esc_html__( 'Network error occurred', 'activetrail-for-woocommerce' ) ); $refreshing = esc_js( esc_html__( 'Refreshing...', 'activetrail-for-woocommerce' ) ); return "jQuery(document).ready(function($) { var baseUrl = '{$base_url}'; // Copy URL functionality. $('.copy-url-btn').on('click', function(e) { e.preventDefault(); var \$button = $(this); var targetId = \$button.data('clipboard-target'); var \$input = $(targetId); var textToCopy = \$input.val(); if (!textToCopy) { alert('{$no_url}'); return; } // Try modern Clipboard API first. if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(textToCopy).then(function() { var originalText = \$button.html(); \$button.addClass('copied').html('{$copied}'); setTimeout(function() { \$button.removeClass('copied').html(originalText); }, 2000); }).catch(function(err) { console.error('Failed to copy:', err); fallbackCopyText(textToCopy, \$button); }); } else { fallbackCopyText(textToCopy, \$button, \$input); } }); // Fallback copy function. function fallbackCopyText(text, \$button, \$input) { var textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.left = '-999999px'; textarea.style.top = '-999999px'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); try { var successful = document.execCommand('copy'); if (successful) { var originalText = \$button.html(); \$button.addClass('copied').html('{$copied}'); setTimeout(function() { \$button.removeClass('copied').html(originalText); }, 2000); } else { alert('{$failed_copy}'); } } catch (err) { console.error('Fallback copy failed:', err); alert('{$failed_copy}'); } finally { document.body.removeChild(textarea); } } // Refresh key functionality. $('.refresh-key-btn').on('click', function(e) { e.preventDefault(); var \$button = $(this); var feedType = \$button.data('feed-type'); var originalText = \$button.html(); if (!feedType) { alert('{$invalid_feed}'); return; } // Show notice before refresh. if (feedType === 'google') { $('#google-refresh-notice').show(); } else if (feedType === 'facebook') { $('#facebook-refresh-notice').show(); } var ajaxUrl = (typeof activetrail_admin !== 'undefined' && activetrail_admin.ajax_url) ? activetrail_admin.ajax_url : '{$ajax_url}'; var nonce = (typeof activetrail_admin !== 'undefined' && activetrail_admin.nonce) ? activetrail_admin.nonce : '{$nonce}'; \$button.prop('disabled', true).html('{$refreshing}'); $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_refresh_feed_key', feed_type: feedType, nonce: nonce }, success: function(response) { if (response.success) { // Update the hidden key input. if (feedType === 'google') { $('#google-feed-key').val(response.data.new_key); var newUrl = baseUrl + '/?activetrail_export=google&key=' + response.data.new_key; $('#google-feed-url').val(newUrl); } else if (feedType === 'facebook') { $('#facebook-feed-key').val(response.data.new_key); var newUrl = baseUrl + '/?activetrail_export=facebook&key=' + response.data.new_key; $('#facebook-feed-url').val(newUrl); } // Show success message. $('

' + activetrail_admin.strings.key_refreshed + '

') .insertAfter('.activetrail-export-feeds-simple') .delay(5000) .fadeOut(); } else { alert('{$failed_refresh} ' + (response.data || '{$unknown_error}')); } }, error: function(xhr, status, error) { console.error('AJAX Error:', status, error); alert('{$network_error}: ' + error); }, complete: function() { \$button.prop('disabled', false).html(originalText); } }); }); });"; } /** * Get coupon manager CSS * * @return string CSS content */ private function get_coupon_css() { return '.activetrail-coupon-manager { max-width: 100%; margin: 20px 0; } .integration-settings { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 30px; } .integration-settings h3 { margin: 0 0 20px 0; color: #333; display: flex; align-items: center; gap: 10px; } .integration-settings h3:before { content: "🔗"; font-size: 20px; } .integration-info { background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 6px; padding: 15px; } .integration-info p { margin: 10px 0; } .integration-info code { background: #fff; padding: 4px 8px; border-radius: 3px; border: 1px solid #ddd; font-family: monospace; } .integration-info ul { margin: 10px 0; padding-left: 20px; } .integration-info li { margin: 5px 0; } .form-group input, .form-group select { border: 1px solid #ddd; border-radius: 4px; font-size: 14px; width: 100%; box-sizing: border-box; max-width: 100%; } .form-group input:focus, .form-group select:focus { border-color: #0073aa; box-shadow: 0 0 0 1px #0073aa; outline: none; } .form-group .description { color: #666; font-size: 12px; margin: 0; } .checkbox-group { flex-direction: column; align-items: flex-start; gap: 8px; min-height: auto; } .checkbox-group label { display: flex; align-items: center; gap: 8px; font-weight: normal; width: 100%; white-space: nowrap; } .checkbox-group input[type="checkbox"] { margin: 0; width: auto; flex-shrink: 0; } .form-actions { display: flex; gap: 10px; align-items: center; padding-top: 15px; border-top: 1px solid #eee; } .form-actions .button { display: flex; align-items: center; gap: 5px; } .coupons-list { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; } .coupons-list h3 { margin: 0 0 20px 0; color: #333; display: flex; align-items: center; gap: 10px; } .coupons-list h3:before { content: "📋"; font-size: 20px; } .no-coupons-message { text-align: center; padding: 40px 20px; color: #666; background: #f9f9f9; border-radius: 4px; } .coupons-table-container { overflow-x: auto; } .coupons-table { border-collapse: collapse; width: 100%; } .coupons-table th, .coupons-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; vertical-align: middle; } .coupons-table th { background: #f8f9fa; font-weight: 600; color: #333; } .coupon-row.expired { background: #fff2f2; } .status-badge { padding: 4px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; } .status-badge.active { background: #d4edda; color: #155724; } .status-badge.expired { background: #f8d7da; color: #721c24; } .status-badge.used-up { background: #fff3cd; color: #856404; } .expired-badge { background: #dc3232; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: bold; } .coupon-actions { display: flex; gap: 5px; } .coupon-actions .button { padding: 4px 8px; min-height: auto; display: flex; align-items: center; } .coupon-actions .dashicons { font-size: 16px; width: 16px; height: 16px; } .delete-coupon { color: #dc3232; } .delete-coupon:hover { background: #dc3232; color: white; border-color: #dc3232; } @media (max-width: 768px) { .coupon-form-grid { grid-template-columns: 1fr; } .form-actions { flex-direction: column; align-items: stretch; } .coupons-table { font-size: 12px; } .coupons-table th, .coupons-table td { padding: 8px; } }'; } /** * Get coupon manager JavaScript * * @return string JavaScript content */ private function get_coupon_js() { $ajax_url = esc_js( admin_url( 'admin-ajax.php' ) ); $nonce = esc_js( wp_create_nonce( 'activetrail_admin_nonce' ) ); $confirm_delete = esc_js( esc_html__( 'Are you sure you want to delete the coupon', 'activetrail-for-woocommerce' ) ); $failed_delete = esc_js( esc_html__( 'Failed to delete coupon:', 'activetrail-for-woocommerce' ) ); $unknown_error = esc_js( esc_html__( 'Unknown error', 'activetrail-for-woocommerce' ) ); $error_deleting = esc_js( esc_html__( 'Error deleting coupon', 'activetrail-for-woocommerce' ) ); $no_coupons = esc_js( esc_html__( 'No coupons found. Create your first coupon using the form above.', 'activetrail-for-woocommerce' ) ); return "jQuery(document).ready(function($) { // Get AJAX URL and nonce - fallback if activetrail_admin is not defined. var ajaxUrl = (typeof activetrail_admin !== 'undefined' && activetrail_admin.ajax_url) ? activetrail_admin.ajax_url : '{$ajax_url}'; var nonce = (typeof activetrail_admin !== 'undefined' && activetrail_admin.nonce) ? activetrail_admin.nonce : '{$nonce}'; // Reset form button. $('#reset-form').on('click', function() { $('#activetrail-create-coupon-form')[0].reset(); }); // Delete coupon. $('.delete-coupon').on('click', function() { var \$button = $(this); var couponId = \$button.data('coupon-id'); var couponCode = \$button.data('coupon-code'); if (!confirm('{$confirm_delete} \"' + couponCode + '\"?')) { return; } \$button.prop('disabled', true); $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_delete_coupon', nonce: nonce, coupon_id: couponId }, success: function(response) { if (response.success) { // Remove row from table. \$button.closest('tr').fadeOut(function() { $(this).remove(); // Check if table is empty. if ($('.coupons-table tbody tr').length === 0) { $('.coupons-table-container').html('

{$no_coupons}

'); } }); } else { alert('{$failed_delete} ' + (response.data || '{$unknown_error}')); } }, error: function() { alert('{$error_deleting}'); }, complete: function() { \$button.prop('disabled', false); } }); }); });"; } /** * Get historical sync CSS * * @return string CSS content */ private function get_historical_sync_css() { return '.activetrail-historical-sync-container { max-width: 100%; margin: 20px 0; } .historical-sync-main { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; } .sync-header h3 { margin: 0 0 10px 0; font-size: 18px; display: flex; align-items: center; } .sync-header .description { margin: 0 0 20px 0; color: #666; line-height: 1.5; } .sync-configuration { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 20px; margin-bottom: 20px; } .config-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; } .config-item { display: flex; flex-direction: column; gap: 5px; } .config-item label { font-weight: 600; color: #333; } .config-item input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .sync-actions { display: flex; gap: 10px; align-items: center; } .sync-actions .button { display: flex; align-items: center; gap: 8px; padding: 10px 16px; cursor: pointer; position: relative; z-index: 1; } .sync-actions .button:disabled { cursor: not-allowed; opacity: 0.6; } .sync-actions .button:not(:disabled):hover { opacity: 0.9; } .sync-actions .button .dashicons.spin { display: inline-block; animation: spin 1s linear infinite; } .sync-progress { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 20px; margin-bottom: 20px; } .sync-progress h4 { margin: 0 0 15px 0; color: #333; } .progress-messages { border-top: 1px solid #e9ecef; padding-top: 15px; } #sync-status-message { padding: 10px; border-radius: 4px; margin-bottom: 10px; } #sync-status-message.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; } #sync-status-message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } #sync-status-message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } #sync-status-message.warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; } #sync-errors h5 { margin: 0 0 10px 0; color: #721c24; } #error-list { margin: 0; padding-left: 20px; max-height: 150px; overflow-y: auto; } #error-list li { margin: 5px 0; color: #721c24; font-size: 13px; } @media (max-width: 768px) { .config-grid { grid-template-columns: 1fr; } .sync-actions { flex-direction: column; align-items: stretch; } .sync-actions .button { margin-bottom: 10px; } } .spin { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }'; } /** * Get historical sync JavaScript * * @return string JavaScript content */ private function get_historical_sync_js() { $ajax_url = esc_js( admin_url( 'admin-ajax.php' ) ); $nonce = esc_js( wp_create_nonce( 'activetrail_admin_nonce' ) ); $sync_completed = esc_js( esc_html__( 'Sync completed successfully!', 'activetrail-for-woocommerce' ) ); $contact = esc_js( esc_html__( 'contact', 'activetrail-for-woocommerce' ) ); $contacts = esc_js( esc_html__( 'contacts', 'activetrail-for-woocommerce' ) ); $failed = esc_js( esc_html__( 'failed', 'activetrail-for-woocommerce' ) ); return "jQuery(document).ready(function($) { console.log('[ActiveTrail Historical Sync] JavaScript loaded'); var syncInterval = null; var isPolling = false; // Handler function for sync button clicks function handleSyncClick(e) { e.preventDefault(); e.stopPropagation(); var \$button = $(this); console.log('[ActiveTrail Historical Sync] Button clicked:', \$button.attr('id')); // Check if button is disabled if (\$button.prop('disabled')) { console.log('[ActiveTrail Historical Sync] Button is disabled'); return false; } var syncType = \$button.data('sync-type'); var ajaxUrl = (typeof activetrail_admin !== 'undefined' && activetrail_admin.ajax_url) ? activetrail_admin.ajax_url : '{$ajax_url}'; var nonce = (typeof activetrail_admin !== 'undefined' && activetrail_admin.nonce) ? activetrail_admin.nonce : '{$nonce}'; // Get date range from form inputs var dateFrom = $('#sync-date-from').val() || ''; var dateTo = $('#sync-date-to').val() || ''; // Determine sync options based on sync type var syncContacts = (syncType === 'contacts' || syncType === 'all'); var syncOrders = (syncType === 'all'); // Disable buttons and show loading state \$button.prop('disabled', true); var originalText = \$button.html(); \$button.html(' ' + (syncType === 'contacts' ? 'Starting Contacts Sync...' : 'Starting Full Sync...')); console.log('[ActiveTrail Historical Sync] Starting sync:', { syncType: syncType, dateFrom: dateFrom, dateTo: dateTo, syncContacts: syncContacts, syncOrders: syncOrders }); $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_start_historical_sync', sync_type: syncType, date_from: dateFrom, date_to: dateTo, sync_contacts: syncContacts, sync_orders: syncOrders, nonce: nonce }, success: function(response) { console.log('[ActiveTrail Historical Sync] Response:', response); if (response.success) { // Show status message container $('#sync-progress-container').show(); $('#start-sync-contacts-only, #start-sync-contacts-orders').hide(); $('#cancel-historical-sync').show(); // Show starting message var \$messageDiv = $('#sync-status-message'); \$messageDiv.removeClass('info success error warning').addClass('info'); \$messageDiv.html('מתחיל...').show(); // Check sync status immediately to update from starting to in process $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_get_sync_progress', nonce: nonce }, success: function(statusResponse) { if (statusResponse && statusResponse.success && statusResponse.data) { if (typeof ActiveTrailAdmin !== 'undefined' && typeof ActiveTrailAdmin.updateSyncProgress === 'function') { ActiveTrailAdmin.updateSyncProgress(statusResponse.data); } else if (typeof updateProgress === 'function') { updateProgress(statusResponse.data); } } } }); // Start progress polling only when button is clicked if (typeof window.activetrailStartProgressPolling === 'function') { window.activetrailStartProgressPolling(); } else if (typeof ActiveTrailAdmin !== 'undefined' && typeof ActiveTrailAdmin.startProgressPolling === 'function') { ActiveTrailAdmin.startProgressPolling(); } else if (typeof startProgressPolling === 'function') { startProgressPolling(); } } else { // Show error message var errorMsg = response.data && response.data.message ? response.data.message : 'Failed to start sync'; alert('Error: ' + errorMsg); // Re-enable buttons \$button.prop('disabled', false).html(originalText); } }, error: function(xhr, status, error) { console.error('[ActiveTrail Historical Sync] AJAX error:', {xhr: xhr, status: status, error: error}); alert('Network error: ' + error); // Re-enable buttons \$button.prop('disabled', false).html(originalText); } }); return false; } // Use event delegation - this works even if buttons are added dynamically $(document).on('click', '#start-sync-contacts-only, #start-sync-contacts-orders', handleSyncClick); // Also listen for the .sync namespace trigger from onclick $(document).on('click.sync', '#start-sync-contacts-only, #start-sync-contacts-orders', handleSyncClick); // Also try direct binding after page loads function initDirectHandlers() { var \$contactsBtn = $('#start-sync-contacts-only'); var \$ordersBtn = $('#start-sync-contacts-orders'); console.log('[ActiveTrail Historical Sync] Initializing handlers:', { contactsExists: \$contactsBtn.length > 0, ordersExists: \$ordersBtn.length > 0 }); if (\$contactsBtn.length > 0) { \$contactsBtn.off('click.directSync').on('click.directSync', handleSyncClick); console.log('[ActiveTrail Historical Sync] Direct handler attached to contacts button'); } if (\$ordersBtn.length > 0) { \$ordersBtn.off('click.directSync').on('click.directSync', handleSyncClick); console.log('[ActiveTrail Historical Sync] Direct handler attached to orders button'); } } // Initialize immediately and after delays initDirectHandlers(); setTimeout(initDirectHandlers, 500); setTimeout(initDirectHandlers, 1000); setTimeout(initDirectHandlers, 2000); // Check if sync is already running when page loads function checkSyncStatus() { var ajaxUrl = (typeof activetrail_admin !== 'undefined' && activetrail_admin.ajax_url) ? activetrail_admin.ajax_url : '{$ajax_url}'; var nonce = (typeof activetrail_admin !== 'undefined' && activetrail_admin.nonce) ? activetrail_admin.nonce : '{$nonce}'; $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_get_sync_progress', nonce: nonce }, success: function(response) { if (response.success && response.data) { var status = response.data.status; if (status === 'running') { // Sync is running - show status message and start polling $('#sync-progress-container').show(); $('#start-sync-contacts-only, #start-sync-contacts-orders').hide(); $('#cancel-historical-sync').show(); updateProgress(response.data); startProgressPolling(); } else if (status === 'completed' || status === 'cancelled') { // Show last status message $('#sync-progress-container').show(); $('#start-sync-contacts-only, #start-sync-contacts-orders').show(); $('#cancel-historical-sync').hide(); updateProgress(response.data); } else { // No sync running - hide progress container $('#sync-progress-container').hide(); } } else { // No sync data - hide progress container $('#sync-progress-container').hide(); } }, error: function(xhr, status, error) { console.error('[ActiveTrail] Error checking sync status:', error); // Hide progress container on error $('#sync-progress-container').hide(); } }); } // Don't check sync status on page load - only when button is clicked // Cancel sync - use event delegation. $(document).on('click', '#cancel-historical-sync', function(e) { e.preventDefault(); e.stopPropagation(); var \$button = $(this); var ajaxUrl = (typeof activetrail_admin !== 'undefined' && activetrail_admin.ajax_url) ? activetrail_admin.ajax_url : '{$ajax_url}'; var nonce = (typeof activetrail_admin !== 'undefined' && activetrail_admin.nonce) ? activetrail_admin.nonce : '{$nonce}'; // Disable button and show loading \$button.prop('disabled', true); var originalText = \$button.html(); \$button.html(' Cancelling...'); console.log('Cancelling historical sync'); $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_cancel_historical_sync', nonce: nonce }, success: function(response) { console.log('Cancel sync response:', response); // Stop progress polling if (typeof window.activetrailStopProgressPolling === 'function') { window.activetrailStopProgressPolling(); } else if (typeof stopProgressPolling === 'function') { stopProgressPolling(); } else if (typeof ActiveTrailAdmin !== 'undefined' && typeof ActiveTrailAdmin.stopProgressPolling === 'function') { ActiveTrailAdmin.stopProgressPolling(); } // Reset sync interface if (typeof resetSyncInterface === 'function') { resetSyncInterface(); } else { // Manual reset if function doesn't exist $('#start-sync-contacts-only, #start-sync-contacts-orders').show(); $('#cancel-historical-sync').hide(); $('#sync-progress-container').hide(); } \$button.prop('disabled', false).html(originalText); }, error: function(xhr, status, error) { console.error('AJAX error cancelling sync:', {xhr: xhr, status: status, error: error}); alert('Error cancelling sync: ' + error); \$button.prop('disabled', false).html(originalText); } }); return false; }); // Start progress polling. function startProgressPolling() { if (isPolling) return; isPolling = true; var ajaxUrl = (typeof activetrail_admin !== 'undefined' && activetrail_admin.ajax_url) ? activetrail_admin.ajax_url : '{$ajax_url}'; var nonce = (typeof activetrail_admin !== 'undefined' && activetrail_admin.nonce) ? activetrail_admin.nonce : '{$nonce}'; // Make functions globally accessible window.activetrailStartProgressPolling = startProgressPolling; window.activetrailStopProgressPolling = stopProgressPolling; window.activetrailResetSyncInterface = resetSyncInterface; // Start polling every 2 seconds syncInterval = setInterval(function() { $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_get_sync_progress', nonce: nonce }, success: function(response) { if (response && response.success) { if (response.data) { updateProgress(response.data); if (response.data.status === 'completed' || response.data.status === 'cancelled') { stopProgressPolling(); if (response.data.status === 'completed') { showSyncMessage('{$sync_completed}', 'success'); setTimeout(function() { location.reload(); }, 2000); } resetSyncInterface(); } } else { console.warn('[ActiveTrail] Progress response missing data:', response); } } else { console.warn('[ActiveTrail] Progress response not successful:', response); } }, error: function(xhr, status, error) { console.error('[ActiveTrail] Error fetching sync progress:', {xhr: xhr, status: status, error: error}); // Don't stop polling on error - might be temporary network issue } }); }, 2000); } // Stop progress polling. function stopProgressPolling() { if (syncInterval) { clearInterval(syncInterval); syncInterval = null; } isPolling = false; } // Update progress display - simplified to show only status messages function updateProgress(data) { if (!data) { console.warn('[ActiveTrail] updateProgress called with no data'); return; } var status = data.status || ''; var \$messageDiv = $('#sync-status-message'); var message = ''; var messageType = 'info'; // Show simple status messages based on sync status if (status === 'running') { message = 'בתהליך...'; messageType = 'info'; } else if (status === 'completed') { message = 'הסתיים בהצלחה'; messageType = 'success'; } else if (status === 'cancelled') { message = 'בוטל'; messageType = 'warning'; } else if (status === 'error' || status === 'failed') { message = 'נכשל'; messageType = 'error'; } // Update message display \$messageDiv.removeClass('info success error warning').addClass(messageType); \$messageDiv.html(message).show(); // Show errors if any if (data.errors && data.errors.length > 0) { var errorList = $('#error-list'); errorList.empty(); data.errors.forEach(function(error) { var errorText = error.email ? (error.email + ': ') : ''; errorText += error.error || error.message || 'Unknown error'; errorList.append('
  • ' + errorText + '
  • '); }); $('#sync-errors').show(); } else { $('#sync-errors').hide(); } // Show failed contacts CSV if available if (data.failed_contacts_csv && data.failed_contacts_csv.url) { $('#download-failed-csv').attr('href', data.failed_contacts_csv.url); if (data.failed_contacts_csv.count) { var contactText = data.failed_contacts_csv.count === 1 ? '{$contact}' : '{$contacts}'; $('#failed-csv-count').text('(' + data.failed_contacts_csv.count + ' ' + contactText + ' ' + '{$failed}' + ')'); } $('#failed-contacts-csv').show(); } else { $('#failed-contacts-csv').hide(); } } // Show sync message. function showSyncMessage(message, type) { var \$messageDiv = $('#sync-status-message'); \$messageDiv.removeClass('info success error warning').addClass(type); \$messageDiv.html(message).show(); } // Format number with commas. function formatNumber(num) { if (num === null || num === undefined || num === '' || num === '-') return '-'; var number = parseInt(num, 10); if (isNaN(number)) return '-'; return number.toLocaleString(); } // Reset sync interface. function resetSyncInterface() { $('#start-sync-contacts-only, #start-sync-contacts-orders').show(); $('#cancel-historical-sync').hide(); $('#sync-progress-container').hide(); } });"; } /** * Get API key field JavaScript * * @return string JavaScript content */ private function get_api_key_field_js() { $ajax_url = esc_js( admin_url( 'admin-ajax.php' ) ); $nonce = esc_js( wp_create_nonce( 'activetrail_admin_nonce' ) ); $field_key = 'activetrail_api_key'; $save_key_label = esc_js( esc_html__( 'Save API Key', 'activetrail-for-woocommerce' ) ); $api_key_required = esc_js( esc_html__( 'API Key is required. Please enter your API key.', 'activetrail-for-woocommerce' ) ); $saving_api_key = esc_js( esc_html__( 'Saving API key...', 'activetrail-for-woocommerce' ) ); $api_key_saved = esc_js( esc_html__( 'API key saved successfully!', 'activetrail-for-woocommerce' ) ); $network_error = esc_js( esc_html__( 'Network error occurred', 'activetrail-for-woocommerce' ) ); $timeout_error = esc_js( esc_html__( 'Request timeout. The API key test took too long. Please check your connection and try again.', 'activetrail-for-woocommerce' ) ); $select_group = esc_js( esc_html__( 'Select a group...', 'activetrail-for-woocommerce' ) ); return "jQuery(document).ready(function($) { console.log('[ActiveTrail] API Key Save JavaScript loaded'); var ajaxUrl = (typeof activetrail_admin !== 'undefined' && activetrail_admin.ajax_url) ? activetrail_admin.ajax_url : '{$ajax_url}'; var nonce = (typeof activetrail_admin !== 'undefined' && activetrail_admin.nonce) ? activetrail_admin.nonce : '{$nonce}'; var \$saveKeyBtn = $('#activetrail-save-api-key-btn'); var saveKeyOriginalText = \$saveKeyBtn.length ? \$saveKeyBtn.text() : '{$save_key_label}'; if (\$saveKeyBtn.length) { console.log('[ActiveTrail] Save API key button found in DOM'); } else { console.warn('[ActiveTrail] Save API key button NOT found in DOM'); } function refreshGroupsDropdown() { var \$groupSelect = $('#activetrail_group_id'); if (\$groupSelect.length === 0) { console.log('[ActiveTrail] Group select not found, skipping refresh'); return; } console.log('[ActiveTrail] Refreshing groups dropdown...'); var originalValue = \$groupSelect.val(); \$groupSelect.prop('disabled', true); var savedApiKey = $('#activetrail_api_key').val(); if (!savedApiKey) { console.log('[ActiveTrail] No API key found, cannot refresh groups'); \$groupSelect.prop('disabled', false); return; } $.ajax({ url: ajaxUrl, type: 'POST', timeout: 15000, data: { action: 'activetrail_get_groups', api_key: savedApiKey, nonce: nonce }, success: function(response) { if (response && response.success && response.data && response.data.groups) { \$groupSelect.empty(); var selectGroupText = '{$select_group}'; \$groupSelect.append(jQuery('').attr('value', '').text(selectGroupText)); jQuery.each(response.data.groups, function(index, group) { var option = jQuery('').attr('value', group.id).text(group.name); if (group.id == originalValue) { option.attr('selected', 'selected'); } \$groupSelect.append(option); }); // Always append Load-more so user can page through groups. \$groupSelect.data('at-page', 1); \$groupSelect.append(jQuery('').attr('value', '__load_more__').text('\u2500\u2500 Load more... \u2500\u2500')); console.log('[ActiveTrail] Groups dropdown refreshed successfully'); } else { console.error('[ActiveTrail] Failed to refresh groups:', response); } }, error: function(xhr, status, error) { console.error('[ActiveTrail] Error refreshing groups:', status, error); }, complete: function() { \$groupSelect.prop('disabled', false); } }); } // Use event delegation like the test connection button (works even if button is added dynamically) $(document).on('click', '#activetrail-save-api-key-btn', function(e) { e.preventDefault(); e.stopPropagation(); console.log('[ActiveTrail] Save API key handler triggered'); var \$button = $(this); var \$input = $('#activetrail_api_key'); var \$status = $('#activetrail-api-key-status'); var apiKeyValue = \$input.val(); console.log('[ActiveTrail] Save API key button clicked'); if (!apiKeyValue || apiKeyValue.trim() === '') { \$status.html('

    {$api_key_required}

    '); \$input.focus(); return false; } // Check if button is disabled if (\$button.prop('disabled')) { console.log('[ActiveTrail] Save button is disabled'); return false; } \$button.prop('disabled', true); var originalText = \$button.text(); \$button.text('{$saving_api_key}'); \$status.html('

    {$saving_api_key}

    '); console.log('[ActiveTrail] Starting API key save request...'); var ajaxRequest = $.ajax({ url: ajaxUrl, type: 'POST', timeout: 35000, data: { action: 'activetrail_save_api_key', api_key: apiKeyValue, nonce: nonce }, success: function(response) { console.log('[ActiveTrail] API key save response:', response); if (response && response.success) { \$status.html('

    ✅ {$api_key_saved}

    '); refreshGroupsDropdown(); // Unlock the Group ID section now that the API key is saved \$('#activetrail-group-section-wrapper').removeClass('at-group-locked'); } else { var errorMessage = (response && response.data) ? response.data : 'Unknown error'; if (typeof errorMessage === 'object' && errorMessage.message) { errorMessage = errorMessage.message; } \$status.html('

    ❌ ' + errorMessage + '

    '); } }, error: function(xhr, status, error) { console.error('[ActiveTrail] API key save error:', status, error, xhr); var errorMessage = '{$network_error}'; if (status === 'timeout') { errorMessage = '{$timeout_error}'; } else { if (xhr.responseJSON && xhr.responseJSON.data) { errorMessage = xhr.responseJSON.data; } else if (xhr.responseText) { try { var response = JSON.parse(xhr.responseText); if (response.data) { errorMessage = response.data; } } catch (e) { console.error('[ActiveTrail] Failed to parse error response:', e); } } } \$status.html('

    ❌ ' + errorMessage + '

    '); }, complete: function() { console.log('[ActiveTrail] API key save request completed'); \$button.prop('disabled', false).text(saveKeyOriginalText || '{$save_key_label}'); } }); return false; }); });"; } /** * Get group select field JavaScript * * @return string JavaScript content */ private function get_group_select_field_js() { $ajax_url = esc_js( admin_url( 'admin-ajax.php' ) ); $nonce = esc_js( wp_create_nonce( 'activetrail_admin_nonce' ) ); $field_key = 'activetrail_group_id'; $group_required = esc_js( esc_html__( 'Group ID is required. Please select a group.', 'activetrail-for-woocommerce' ) ); $saving_group = esc_js( esc_html__( 'Saving group...', 'activetrail-for-woocommerce' ) ); $group_saved = esc_js( esc_html__( 'Group saved successfully!', 'activetrail-for-woocommerce' ) ); $network_error = esc_js( esc_html__( 'Network error occurred', 'activetrail-for-woocommerce' ) ); $save_group = esc_js( esc_html__( 'Save Group', 'activetrail-for-woocommerce' ) ); $saving = esc_js( esc_html__( 'Saving...', 'activetrail-for-woocommerce' ) ); $debug_info = esc_js( esc_html__( 'Debug Info:', 'activetrail-for-woocommerce' ) ); return "jQuery(document).ready(function($) { var ajaxUrl = (typeof activetrail_admin !== 'undefined' && activetrail_admin.ajax_url) ? activetrail_admin.ajax_url : '{$ajax_url}'; var nonce = (typeof activetrail_admin !== 'undefined' && activetrail_admin.nonce) ? activetrail_admin.nonce : '{$nonce}'; $('#activetrail-save-group-btn').on('click', function(e) { e.preventDefault(); e.stopPropagation(); var \$button = $(this); var \$select = $('#activetrail_group_id'); var \$status = $('#activetrail-group-status'); var selectedValue = \$select.val(); var selectedText = \$select.find('option:selected').text(); if (!selectedValue || selectedValue === '') { \$status.html('

    {$group_required}

    '); \$select.focus(); return; } \$button.prop('disabled', true).text('{$saving}'); \$status.html('

    {$saving_group}

    '); $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_save_group', group_id: selectedValue, nonce: nonce }, success: function(response) { if (response.success) { \$status.html('

    ✅ {$group_saved} (' + selectedText + ')

    '); updateDebugInfo(); console.log('[ActiveTrail] Group saved:', { group_id: selectedValue, group_name: selectedText, timestamp: new Date().toISOString() }); // Show Connect Store section now that a group is saved \$('#activetrail-connect-store-section-wrapper').slideDown(300); // Enable Connect Store button now that a group is saved var \$connectBtn = \$('#activetrail-connect-store-btn'); if (!\$connectBtn.hasClass('at-connect-ready')) { \$connectBtn.prop('disabled', false).addClass('at-connect-ready'); } } else { \$status.html('

    ' + (response.data || 'Unknown error') + '

    '); } }, error: function() { \$status.html('

    ❌ {$network_error}

    '); }, complete: function() { \$button.prop('disabled', false).text('{$save_group}'); } }); }); function updateDebugInfo() { var \$debug = $('#activetrail-group-debug'); var selectedValue = $('#activetrail_group_id').val(); $.ajax({ url: ajaxUrl, type: 'POST', data: { action: 'activetrail_get_debug_info', nonce: nonce }, success: function(response) { if (response.success && response.data) { var debugHtml = '{$debug_info}
    '; debugHtml += 'Current WC Option: ' + (response.data.wc_option || 'empty') + '
    '; debugHtml += 'Unified Settings: ' + (response.data.unified_setting || 'empty') + '
    '; debugHtml += 'Selected Value: ' + (selectedValue || 'empty') + '
    '; debugHtml += 'Sync Status: ' + (response.data.sync_status || 'unknown') + '
    '; debugHtml += 'Last Updated: ' + response.data.timestamp + '
    '; \$debug.html(debugHtml); } } }); } });"; } /** * Get required field indicators JavaScript * * @return string JavaScript content */ private function get_required_field_indicators_js() { return "jQuery(document).ready(function($) { // Add asterisk to API Key label. var \$apiKeyLabel = $('label[for=\"activetrail_api_key\"]'); if (\$apiKeyLabel.length && \$apiKeyLabel.find('.required').length === 0) { \$apiKeyLabel.append(' *'); } // Add required attribute to API Key field. var \$apiKeyField = $('#activetrail_api_key'); if (\$apiKeyField.length && !\$apiKeyField.attr('required')) { \$apiKeyField.attr('required', 'required'); \$apiKeyField.attr('aria-required', 'true'); } });"; } /** * Display admin notices */ public function admin_notices() { // Only show on our settings page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation only, no data processing if (!isset($_GET['page']) || sanitize_text_field(wp_unslash($_GET['page'])) !== 'wc-settings') { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation only, no data processing if (!isset($_GET['tab']) || sanitize_text_field(wp_unslash($_GET['tab'])) !== 'activetrail') { return; } // Check configuration status. $status = ActiveTrail_Settings::get_configuration_status(); if (!$status['fully_configured']) { ?>

    '; } /** * Output closing wrapper div for a collapsible section. * * @param array $value Field configuration. */ public function output_section_wrapper_end( $value ) { echo '
    '; } /** * Open the Group section wrapper div (fires after API settings table closes). */ public function output_group_section_wrapper_open() { echo '
    '; } /** * Close Group wrapper and open Connect Store wrapper (fires after Group settings table closes). */ public function output_group_section_wrapper_close_connect_open() { echo '
    '; echo '
    '; } /** * Close the Connect Store section wrapper div (fires after Connect Store settings table closes). */ public function output_connect_store_section_wrapper_close() { echo '
    '; } /** * Output Connect Store button field. * * @param array $value Field configuration. */ public function output_connect_store_button_field( $value ) { // Button has been moved to the API Key field row. } /** * Output group select field with save button and debug * * @param array $value Field configuration. */ public function output_group_select_field($value) { $field_key = $value['id']; $field_name = $value['name']; $description = $value['desc']; $options = $value['options']; $default_value = $value['default']; $css = isset($value['css']) ? $value['css'] : ''; // Get current value. $current_value = get_option($field_key, $default_value); ?>

    $status_info ) { $cat = $status_info['category']; if ( ! isset( $grouped_statuses[ $cat ] ) ) { $grouped_statuses[ $cat ] = array(); } $grouped_statuses[ $cat ][ $status_key ] = $status_info; } $super_groups = array( 'pending_group' => array( 'categories' => array( 'pending', 'on-hold' ), 'label' => esc_html__( 'Pending Orders', 'activetrail-for-woocommerce' ), ), 'active_group' => array( 'categories' => array( 'processing' ), 'label' => esc_html__( 'Active Orders', 'activetrail-for-woocommerce' ), ), 'completed_group' => array( 'categories' => array( 'completed' ), 'label' => esc_html__( 'Completed Orders', 'activetrail-for-woocommerce' ), ), 'cancelled_group' => array( 'categories' => array( 'cancelled', 'failed', 'refunded' ), 'label' => esc_html__( 'Cancelled/Failed Orders', 'activetrail-for-woocommerce' ), ), 'custom_group' => array( 'categories' => array( 'custom' ), 'label' => esc_html__( 'Custom Statuses', 'activetrail-for-woocommerce' ), ), ); $badge_labels = array( 'pending' => esc_html__( 'Pending', 'activetrail-for-woocommerce' ), 'on-hold' => esc_html__( 'Pending', 'activetrail-for-woocommerce' ), 'processing' => esc_html__( 'Active', 'activetrail-for-woocommerce' ), 'completed' => esc_html__( 'Completed', 'activetrail-for-woocommerce' ), 'cancelled' => esc_html__( 'Cancelled', 'activetrail-for-woocommerce' ), 'failed' => esc_html__( 'Cancelled', 'activetrail-for-woocommerce' ), 'refunded' => esc_html__( 'Cancelled', 'activetrail-for-woocommerce' ), 'custom' => esc_html__( 'Custom', 'activetrail-for-woocommerce' ), ); ?>

    $super_group ) : $has_statuses = false; foreach ( $super_group['categories'] as $cat ) { if ( isset( $grouped_statuses[ $cat ] ) ) { $has_statuses = true; break; } } if ( ! $has_statuses ) continue; ?> $status_info ) : $is_synced = empty( $enabled_statuses ) || in_array( $status_key, $enabled_statuses, true ); $row_class = 'activetrail-status-row'; $row_class .= $is_synced ? ' sync-enabled' : ' sync-disabled'; $badge_label = isset( $badge_labels[ $category ] ) ? $badge_labels[ $category ] : esc_html( ucfirst( str_replace( '-', ' ', $category ) ) ); ?>
    '' . esc_html__('API Documentation', 'activetrail-for-woocommerce') . '', 'support' => '' . esc_html__('Support', 'activetrail-for-woocommerce') . '', ); return array_merge($links, $row_meta); } return $links; } /** * AJAX handler to save API key */ public function ajax_save_api_key() { try { // Verify nonce. $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (empty($nonce) || !wp_verify_nonce($nonce, 'activetrail_admin_nonce')) { wp_send_json_error(esc_html__('Security check failed', 'activetrail-for-woocommerce')); return; } // Check permissions. if (!current_user_can('manage_options')) { wp_send_json_error(esc_html__('Insufficient permissions', 'activetrail-for-woocommerce')); return; } $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; if (empty($api_key) || trim($api_key) === '') { wp_send_json_error(esc_html__('API Key is required. Please enter your API key.', 'activetrail-for-woocommerce')); return; } // Test the API key before saving. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php'; $api_client = new ActiveTrail_API_Client($api_key); // Set a timeout for the test connection (reduced from 30 to 15 seconds). // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Necessary to prevent timeout during API key validation set_time_limit(15); // Use error handling to catch any exceptions. $test_result = array('success' => false, 'message' => ''); try { $test_result = $api_client->test_connection(); } catch (Exception $e) { $test_result = array( 'success' => false, 'message' => esc_html__('Error testing API connection: ', 'activetrail-for-woocommerce') . $e->getMessage() ); } if (!$test_result['success']) { // API key is invalid. $error_message = isset($test_result['message']) && !empty($test_result['message']) ? $test_result['message'] : esc_html__('Invalid API key. Please check your API key and try again.', 'activetrail-for-woocommerce'); // Try to parse JSON error message if it's escaped JSON string. $decoded_message = json_decode($error_message, true); if ($decoded_message && isset($decoded_message['detail'])) { $error_message = $decoded_message['detail']; } elseif ($decoded_message && isset($decoded_message['title'])) { $error_message = $decoded_message['title']; } // Simplify error message for user. if (strpos($error_message, 'ResourceNotFound') !== false || strpos($error_message, '404') !== false) { $error_message = esc_html__('Invalid API key. Please check your API key and try again.', 'activetrail-for-woocommerce'); } wp_send_json_error($error_message); return; } // API key is valid, save it. update_option('activetrail_api_key', $api_key); ActiveTrail_Settings::update('api_key', $api_key); // API test passed and key is saved — always report success. // update_option() returns false on "no change" (same key already stored), // which is not an error condition. wp_send_json_success(array( 'message' => esc_html__('API key saved successfully', 'activetrail-for-woocommerce'), 'api_key' => substr($api_key, 0, 4) . '...' . substr($api_key, -4) )); } catch (Exception $e) { wp_send_json_error(esc_html__('An error occurred: ', 'activetrail-for-woocommerce') . $e->getMessage()); } } /** * AJAX handler to connect store */ public function ajax_connect_store() { try { $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (empty($nonce) || !wp_verify_nonce($nonce, 'activetrail_admin_nonce')) { wp_send_json_error(esc_html__('Security check failed', 'activetrail-for-woocommerce')); return; } if (!current_user_can('manage_options')) { wp_send_json_error(esc_html__('Insufficient permissions', 'activetrail-for-woocommerce')); return; } $api_key = ActiveTrail_Settings::get('api_key'); if (empty($api_key)) { wp_send_json_error(esc_html__('API key not configured', 'activetrail-for-woocommerce')); return; } require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php'; $api_client = new ActiveTrail_API_Client($api_key); $result = $api_client->connect_store(); if ($result['success']) { wp_send_json_success(array('message' => $result['message'])); } else { wp_send_json_error($result['message']); } } catch (Exception $e) { wp_send_json_error(esc_html__('An error occurred: ', 'activetrail-for-woocommerce') . $e->getMessage()); } } /** * AJAX handler to save group */ public function ajax_save_group() { // Verify nonce. $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (empty($nonce) || !wp_verify_nonce($nonce, 'activetrail_admin_nonce')) { wp_die('Security check failed'); } // Check permissions. if (!current_user_can('manage_options')) { wp_die('Insufficient permissions'); } $group_id = isset($_POST['group_id']) ? sanitize_text_field(wp_unslash($_POST['group_id'])) : ''; if (empty($group_id)) { wp_send_json_error('Group ID is required'); } // Save the group ID. // update_option returns false if value is unchanged — treat as success if the value is already set correctly. ActiveTrail_Settings::update('group_id', $group_id); $saved = ActiveTrail_Settings::get('group_id'); if ($saved === $group_id) { wp_send_json_success(array( 'message' => 'Group saved successfully', 'group_id' => $group_id )); } else { wp_send_json_error('Failed to save group'); } } /** * AJAX handler to get debug info */ public function ajax_get_debug_info() { // Verify nonce. $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (empty($nonce) || !wp_verify_nonce($nonce, 'activetrail_admin_nonce')) { wp_die('Security check failed'); } // Check permissions. if (!current_user_can('manage_options')) { wp_die('Insufficient permissions'); } $debug_info = array( 'wc_option' => get_option('activetrail_group_id', 'empty'), 'unified_setting' => ActiveTrail_Settings::get('group_id', 'empty'), 'sync_status' => ActiveTrail_Settings::is_configured() ? 'configured' : 'not_configured', 'timestamp' => current_time('mysql') ); wp_send_json_success($debug_info); } /** * AJAX handler to test API connection */ public function ajax_test_api() { // Verify nonce. $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (empty($nonce) || !wp_verify_nonce($nonce, 'activetrail_admin_nonce')) { wp_die('Security check failed'); } // Check permissions. if (!current_user_can('manage_options')) { wp_die('Insufficient permissions'); } $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; if (empty($api_key)) { wp_send_json_error('API key is required'); } // Log the start of the test process. if (function_exists('activetrail_logger')) { activetrail_logger()->info('Starting API connection test with key: ' . substr($api_key, 0, 4) . '...' . substr($api_key, -4), 'SETTINGS'); } // Test the API connection. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php'; $api_client = new ActiveTrail_API_Client($api_key); $result = $api_client->test_connection(); if ($result['success']) { // Update connection status. ActiveTrail_Settings::update_connection_status(true, 'Connection successful'); // Log API connection success. if (function_exists('activetrail_logger')) { activetrail_logger()->info('API Connection Test Successful - Now attempting store connection', 'SETTINGS'); } // Force delete any existing store status to ensure a fresh connection attempt. delete_option('activetrail_store_status'); // ✅ Step 2: Attempt to connect the store. $store_result = $api_client->connect_store(); // Log the store connection attempt result. if (function_exists('activetrail_logger')) { $logger = activetrail_logger(); if (is_array($store_result) && isset($store_result['success'])) { if ($store_result['success']) { $logger->info('Store Connection Successful', 'SETTINGS', $store_result); } else { $logger->error('Store Connection Failed', 'SETTINGS', $store_result); } } else { $logger->warning('Store Connection returned unexpected format', 'SETTINGS', $store_result); } } if (is_array($store_result) && isset($store_result['success']) && !$store_result['success']) { // If store connection failed, show the error but don't fail the overall test. wp_send_json_success([ 'message' => '✅ API key verified, but store connection had issues: ' . $store_result['message'], 'api_success' => true, 'store_success' => false, 'store_response' => $store_result ]); } // Both API key verification and store connection succeeded. wp_send_json_success([ 'message' => '✅ API key verified and store connected successfully!', 'api_success' => true, 'store_success' => true, 'store_response' => $store_result, 'store_status' => get_option('activetrail_store_status', 'unknown') ]); } else { // Ensure we have a valid error message. $error_message = isset($result['message']) && !empty($result['message']) && $result['message'] !== 'undefined' ? $result['message'] : 'Unable to connect to ActiveTrail API. Please check your API key and internet connection.'; // Log the error for debugging. if (function_exists('activetrail_logger')) { activetrail_logger()->error('API Connection Test Failed', 'SETTINGS', $result); } // Update connection status. ActiveTrail_Settings::update_connection_status(false, $error_message); // Send error response with the validated message. wp_send_json_error(array( 'message' => $error_message, 'debug_info' => isset($result['debug_info']) ? $result['debug_info'] : null )); } } /** * AJAX handler to get groups */ public function ajax_get_groups() { // Verify nonce. $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (empty($nonce) || !wp_verify_nonce($nonce, 'activetrail_admin_nonce')) { wp_die('Security check failed'); } // Check permissions. if (!current_user_can('manage_options')) { wp_die('Insufficient permissions'); } $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; if (empty($api_key)) { wp_send_json_error('API key is required'); } $page = isset($_POST['page']) ? absint($_POST['page']) : 1; if ($page < 1) { $page = 1; } // Get groups from API with pagination. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php'; $api_client = new ActiveTrail_API_Client($api_key); $result = $api_client->get_groups(array('Page' => $page, 'Limit' => 20)); if ($result['success']) { // Format groups for select dropdown. $formatted_groups = array(); foreach ($result['data'] as $group) { $id = isset($group['Id']) ? $group['Id'] : (isset($group['id']) ? $group['id'] : ''); $name = isset($group['Name']) ? $group['Name'] : (isset($group['name']) ? $group['name'] : ''); if (!empty($id) && !empty($name)) { $formatted_groups[] = array( 'id' => $id, 'name' => $name, ); } } wp_send_json_success(array( 'groups' => $formatted_groups, 'has_more' => count($formatted_groups) >= 20, 'page' => $page, )); } else { wp_send_json_error($result['message']); } } /** * Update API key field * * @param array $value Field configuration. */ public function update_api_key_field($value) { $field_key = $value['id']; // Only handle ActiveTrail API key field. if ($field_key !== 'activetrail_api_key') { return; } // Verify nonce for security. if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) { return; } if (isset($_POST[$field_key])) { $field_value = sanitize_text_field(wp_unslash($_POST[$field_key])); // Validate required API key field. if (empty($field_value) || trim($field_value) === '') { WC_Admin_Settings::add_error(esc_html__('API Key is required. Please enter your ActiveTrail API key.', 'activetrail-for-woocommerce')); return; } // Save to WooCommerce option. update_option($field_key, $field_value); // Map to unified settings. ActiveTrail_Settings::update('api_key', $field_value); } } /** * Update function for group select field * * @param array $value Field configuration. */ public function update_group_select_field($value) { // Verify nonce for security. if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) { return; } $field_key = $value['id']; if (isset($_POST[$field_key])) { $group_id = sanitize_text_field(wp_unslash($_POST[$field_key])); // Validate that group_id is not empty (required field). if (empty($group_id)) { WC_Admin_Settings::add_error(esc_html__('Group ID is required. Please select a group.', 'activetrail-for-woocommerce')); return; } update_option($field_key, $group_id); // Also update in unified settings. ActiveTrail_Settings::update('group_id', $group_id); } else { // Field is required but not provided. WC_Admin_Settings::add_error(esc_html__('Group ID is required. Please select a group.', 'activetrail-for-woocommerce')); } } /** * Update function for status table field * * @param array $value Field configuration. */ public function update_status_table_field($value) { // Verify nonce for security. if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) { return; } $field_key = $value['id']; // Always enable all statuses - get all available statuses. $detailed_statuses = ActiveTrail_Settings::get_order_statuses_detailed(); $status_mapping = array(); $enabled_statuses = array(); // Mark all statuses as enabled. foreach ($detailed_statuses as $status_key => $status_info) { // Get description from POST if available, otherwise use default. $description = ''; if (isset($_POST[$field_key][$status_key]['description'])) { $description = sanitize_text_field(wp_unslash($_POST[$field_key][$status_key]['description'])); } else { $description = $status_info['description']; } $status_mapping[$status_key] = array( 'sync' => true, // Always sync 'description' => $description ); $enabled_statuses[] = $status_key; } // Save to temporary WooCommerce option (will be processed in process_settings_update). update_option($field_key, $status_mapping); // Also save directly to unified settings. ActiveTrail_Settings::update_status_mapping($status_mapping); ActiveTrail_Settings::update('order_statuses', $enabled_statuses); } /** * Update function for password fields * * @param array $value Field configuration. */ public function update_password_field($value) { // Verify nonce for security. if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) { return; } $field_key = $value['id']; // Only handle ActiveTrail fields. if (strpos($field_key, 'activetrail_') !== 0) { return; } // Validate required API key field. if ( 'activetrail_api_key' === $field_key ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized on next line $field_value_raw = isset($_POST[$field_key]) ? wp_unslash($_POST[$field_key]) : ''; if (empty(trim($field_value_raw))) { WC_Admin_Settings::add_error(esc_html__('API Key is required. Please enter your ActiveTrail API key.', 'activetrail-for-woocommerce')); return; } } if (isset($_POST[$field_key])) { $field_value = sanitize_text_field(wp_unslash($_POST[$field_key])); // Additional validation for API key. if ($field_key === 'activetrail_api_key' && empty($field_value)) { WC_Admin_Settings::add_error(esc_html__('API Key is required. Please enter your ActiveTrail API key.', 'activetrail-for-woocommerce')); return; } // Save to WooCommerce option. update_option($field_key, $field_value); // Map to unified settings. $setting_key = str_replace('activetrail_', '', $field_key); ActiveTrail_Settings::update($setting_key, $field_value); } else { // Field is required but not provided. if ( 'activetrail_api_key' === $field_key ) { WC_Admin_Settings::add_error(esc_html__('API Key is required. Please enter your ActiveTrail API key.', 'activetrail-for-woocommerce')); } } } /** * Output export feeds field * * @param array $value Field configuration. */ public function output_export_feeds_field($value) { // Switch to plugin's language setting and reload text domain for translations. $this->switch_to_plugin_language(); $google_url = $value['google_url']; $facebook_url = $value['facebook_url']; $product_count = $value['product_count']; $last_update = $value['last_update']; $google_key = $value['google_key']; $facebook_key = $value['facebook_key']; ?>

    generate_secure_feed_key(); $option_name = 'activetrail_' . $feed_type . '_feed_key'; // Update the key. update_option($option_name, $new_key); // Generate new URL. $new_url = home_url('?activetrail_export=' . $feed_type . '&key=' . $new_key); wp_send_json_success(array( 'new_key' => $new_key, 'new_url' => $new_url, 'message' => 'Key refreshed successfully' )); } /** * Update function for text fields * * @param array $value Field configuration. */ public function update_text_field($value) { // Verify nonce for security. if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) { return; } $field_key = $value['id']; // Only handle ActiveTrail fields. if (strpos($field_key, 'activetrail_') !== 0) { return; } if (isset($_POST[$field_key])) { $field_value = sanitize_text_field(wp_unslash($_POST[$field_key])); // Save to WooCommerce option. update_option($field_key, $field_value); // Map to unified settings. $setting_key = str_replace('activetrail_', '', $field_key); ActiveTrail_Settings::update($setting_key, $field_value); } } /** * Update function for checkbox fields * * @param array $value Field configuration. */ public function update_checkbox_field($value) { $field_key = $value['id']; // Only handle ActiveTrail fields. if (strpos($field_key, 'activetrail_') !== 0) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce settings form handles nonce verification $checkbox_value = isset($_POST[$field_key]) ? 'yes' : 'no'; // Save to WooCommerce option. update_option($field_key, $checkbox_value); // Map to unified settings. $setting_key = str_replace('activetrail_', '', $field_key); $boolean_value = ($checkbox_value === 'yes'); // Save to unified settings. ActiveTrail_Settings::update($setting_key, $boolean_value); } /** * Update function for textarea fields * * @param array $value Field configuration. */ public function update_textarea_field($value) { // Verify nonce for security. if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) { return; } $field_key = $value['id']; // Only handle ActiveTrail fields. if (strpos($field_key, 'activetrail_') !== 0) { return; } if (isset($_POST[$field_key])) { $field_value = sanitize_textarea_field(wp_unslash($_POST[$field_key])); // Save to WooCommerce option. update_option($field_key, $field_value); // Map to unified settings. $setting_key = str_replace('activetrail_', '', $field_key); ActiveTrail_Settings::update($setting_key, $field_value); } } /** * Update function for number fields * * @param array $value Field configuration. */ public function update_number_field($value) { $field_key = $value['id']; // Only handle ActiveTrail fields. if (strpos($field_key, 'activetrail_') !== 0) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce settings form handles nonce verification if (isset($_POST[$field_key])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce settings form handles nonce verification $field_value = absint(wp_unslash($_POST[$field_key])); // Save to WooCommerce option. update_option($field_key, $field_value); // Map to unified settings. $setting_key = str_replace('activetrail_', '', $field_key); ActiveTrail_Settings::update($setting_key, $field_value); } } /** * Output coupon manager field * * @param array $value Field configuration. */ public function output_coupon_manager_field($value) { // Get existing ActiveTrail coupons. $coupons = get_posts(array( 'post_type' => 'shop_coupon', 'post_status' => 'publish', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query necessary for filtering ActiveTrail coupons 'meta_query' => array( array( 'key' => '_activetrail_coupon', 'value' => 'yes', 'compare' => '=' ) ), 'numberposts' => -1, 'orderby' => 'date', 'order' => 'DESC' )); // Get settings. $settings = ActiveTrail_Settings::get_all(); $endpoint_url = get_rest_url(null, 'activetrail/v1/coupons'); ?>

    • Content-Type: application/json

    ID); $usage_count = $coupon->get_usage_count(); $usage_limit = $coupon->get_usage_limit(); $expiry_date = $coupon->get_date_expires(); $is_expired = $expiry_date && $expiry_date->getTimestamp() < time(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Meta query necessary for coupon data $external_id = get_post_meta($coupon_post->ID, '_activetrail_external_id', true); ?>
    get_code()); ?> get_description()): ?>
    get_description()); ?>
    esc_html__('Percentage', 'activetrail-for-woocommerce'), 'fixed_cart' => esc_html__('Fixed Cart', 'activetrail-for-woocommerce'), 'fixed_product' => esc_html__('Fixed Product', 'activetrail-for-woocommerce') ); echo esc_html($type_labels[$coupon->get_discount_type()] ?? $coupon->get_discount_type()); ?> get_discount_type() ) { echo esc_html($coupon->get_amount() . '%'); } else { echo wp_kses_post(wc_price($coupon->get_amount())); } ?> date_i18n(get_option('date_format'))); if ($is_expired) { echo ' ' . esc_html__('Expired', 'activetrail-for-woocommerce') . ''; } } else { echo esc_html__('No expiry', 'activetrail-for-woocommerce'); } ?> = $usage_limit): ?>
    post_type !== 'shop_coupon') { wp_send_json_error(esc_html__('Coupon not found.', 'activetrail-for-woocommerce')); } try { // Delete the coupon. $result = wp_delete_post($coupon_id, true); if ($result) { wp_send_json_success(array( /* translators: %s: Coupon code */ 'message' => sprintf(esc_html__('Coupon "%s" deleted successfully!', 'activetrail-for-woocommerce'), $coupon_post->post_title) )); } else { wp_send_json_error(esc_html__('Failed to delete coupon.', 'activetrail-for-woocommerce')); } } catch (Exception $e) { /* translators: %s: Error message */ wp_send_json_error(sprintf(esc_html__('Error deleting coupon: %s', 'activetrail-for-woocommerce'), $e->getMessage())); } } /** * Handle coupon form submission (deprecated - now handled via REST API) */ private function handle_coupon_form_submission() { // This method is deprecated - coupon creation is now handled via REST API. // Kept for backward compatibility but does nothing. return; } /** * Maybe create coupon from settings form submission (deprecated) */ private function maybe_create_coupon_from_settings() { // This method is deprecated - coupon creation is now handled via REST API. // Kept for backward compatibility but does nothing. return; } /** * Output historical sync field * * @param array $value Field configuration. */ public function output_historical_sync_field($value) { // Switch to plugin's language setting and reload text domain for translations. $this->switch_to_plugin_language(); // Get current sync status. $sync_status = get_option('activetrail_historical_sync_status', array()); // Check if API is configured. $api_configured = ActiveTrail_Settings::is_api_configured(); ?>


    id !== 'woocommerce_page_wc-settings') { return; } // Only run on ActiveTrail tab. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Navigation only, no data processing if (!isset($_GET['tab']) || $_GET['tab'] !== 'activetrail') { return; } // JavaScript moved to wp_add_inline_script in enqueue_admin_scripts() } /** * Switch to plugin's configured language and reload text domain * * @return bool True if locale/textdomain was loaded, false otherwise */ private function switch_to_plugin_language() { if (!class_exists('ActiveTrail_Settings')) { require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/settings/class-settings.php'; } $current_language = ActiveTrail_Settings::get_current_language(); if (empty($current_language)) { return false; } $domain = 'activetrail-for-woocommerce'; // Find the correct MO file. // Note: MO files are named activetrail-woocommerce-{locale}.mo. $mofile = ACTIVETRAIL_WC_PLUGIN_DIR . 'languages/activetrail-woocommerce-' . $current_language . '.mo'; if (!file_exists($mofile)) { // Fallback to WordPress languages directory. $mofile = WP_LANG_DIR . '/plugins/activetrail-woocommerce-' . $current_language . '.mo'; } if (file_exists($mofile)) { // Completely remove existing text domain from all globals. unload_textdomain($domain); if (isset($GLOBALS['l10n'][$domain])) { unset($GLOBALS['l10n'][$domain]); } if (isset($GLOBALS['l10n_unloaded'])) { unset($GLOBALS['l10n_unloaded'][$domain]); } // Use override_load_textdomain filter to force loading our MO file. $override_filter = function($override, $domain_to_load, $mofile_path) use ($domain, $mofile) { if ($domain_to_load === $domain) { // Load our specific MO file. if (!class_exists('MO')) { require_once ABSPATH . WPINC . '/pomo/mo.php'; } $mo = new MO(); if ($mo->import_from_file($mofile)) { $GLOBALS['l10n'][$domain] = &$mo; return true; // Indicate we handled it } } return $override; }; // Add filter with high priority. add_filter('override_load_textdomain', $override_filter, 999, 3); // Load text domain - this will trigger our filter. load_textdomain($domain, $mofile); // Remove filter to avoid affecting other translations. remove_filter('override_load_textdomain', $override_filter, 999); // Also directly set in globals as backup. if (!class_exists('MO')) { require_once ABSPATH . WPINC . '/pomo/mo.php'; } if (class_exists('MO')) { $mo = new MO(); if ($mo->import_from_file($mofile)) { $GLOBALS['l10n'][$domain] = &$mo; } } // Hardcoded Hebrew fallback for feed labels, order statuses desc, and status table category labels when .mo lookup fails (ensures RTL admin shows Hebrew). $feed_label_fallbacks = array( 'Product feed title.' => 'כותרת פיד המוצרים', 'Product feed description.' => 'תיאור פיד המוצרים', 'Select which order statuses should trigger events to ActiveTrail. If no statuses are selected, all statuses will be synchronized (backward compatible).' => 'בחר אילו סטטוסי הזמנות יפעילו אירועים ל-ActiveTrail. אם לא נבחרו סטטוסים, כל הסטטוסים יסונכרנו (תאימות לאחור).', // Status table category column (קטגוריה). 'Pending' => 'ממתין', 'Processing' => 'בטיפול', 'On Hold' => 'בהשהייה', 'Completed' => 'הושלם', 'Cancelled' => 'בוטל', 'Refunded' => 'הוחזר', 'Failed' => 'נכשל', 'Custom Statuses' => 'סטטוסים מותאמים', ); if ( 'he_IL' === $current_language && ! empty( $feed_label_fallbacks ) ) { add_filter( 'gettext', function( $translated_text, $text, $text_domain ) use ( $domain, $feed_label_fallbacks ) { if ( $text_domain !== $domain ) { return $translated_text; } if ( isset( $feed_label_fallbacks[ $text ] ) && $translated_text === $text ) { return $feed_label_fallbacks[ $text ]; } return $translated_text; }, 10000, 3 ); } // Add gettext filter as additional backup to override translations at runtime. // Use the MO object that we just loaded into globals. if (isset($GLOBALS['l10n'][$domain]) && is_object($GLOBALS['l10n'][$domain])) { $mo_object = $GLOBALS['l10n'][$domain]; // Add filter to use our loaded MO object directly. add_filter('gettext', function($translated_text, $text, $text_domain) use ($domain, $mo_object) { // Only process our domain. if ($text_domain !== $domain) { return $translated_text; } // Use translate method from MO object. if (method_exists($mo_object, 'translate')) { $translation = $mo_object->translate($text); if ($translation !== $text) { return $translation; } } elseif (isset($mo_object->entries[$text])) { $entry = $mo_object->entries[$text]; if (is_object($entry) && isset($entry->translations) && is_array($entry->translations) && !empty($entry->translations)) { return $entry->translations[0]; } } return $translated_text; }, 9999, 3); } else { // Fallback: load translations map. $translations_map = $this->load_translations_from_mo($mofile); if (!empty($translations_map)) { // Store in class property so filter can access it. $this->cached_translations = $translations_map; $this->cached_translations_domain = $domain; add_filter('gettext', function($translated_text, $text, $text_domain) use ($domain, $translations_map) { // Only process our domain. if ($text_domain !== $domain) { return $translated_text; } // Check if we have a translation for this text. if (isset($translations_map[$text]) && !empty($translations_map[$text])) { return $translations_map[$text]; } return $translated_text; }, 9999, 3); } } } // Support WPML if installed. if (function_exists('icl_object_id') && function_exists('do_action')) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WPML core global variable global $sitepress; if ($sitepress) { // Convert language code format if needed (e.g., en_US -> en). $wpml_lang = strstr($current_language, '_', true) ?: $current_language; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML hook do_action('wpml_switch_language', $wpml_lang); } } // Support Polylang if installed. if (function_exists('pll_current_language') && function_exists('PLL')) { $polylang = PLL(); if ($polylang) { // Convert language code format if needed. $pll_lang = strstr($current_language, '_', true) ?: $current_language; $polylang->curlang = $polylang->model->get_language($pll_lang); if (!$polylang->curlang) { // Try full code. $polylang->curlang = $polylang->model->get_language($current_language); } } } return true; } /** * Restore original locale/textdomain (optional - textdomain reload is usually sufficient) */ private function restore_plugin_language() { // For textdomain-based approach, we don't need to restore anything. // The textdomain will be reloaded on next page load with default locale. // WPML and Polylang don't need explicit restoration as they handle it automatically. } /** * Load translations from MO file and return translation map * * @param string $mofile Path to MO file. * @return array Translation map (original => translated) */ private function load_translations_from_mo($mofile) { $translations = array(); if (!file_exists($mofile)) { return $translations; } if (!class_exists('MO')) { require_once ABSPATH . WPINC . '/pomo/mo.php'; } if (class_exists('MO')) { $mo = new MO(); if ($mo->import_from_file($mofile)) { // Extract translations from MO object. // MO class stores entries as: key (singular) => Translation_Entry object. foreach ($mo->entries as $key => $entry) { if (is_object($entry) && isset($entry->translations) && is_array($entry->translations) && !empty($entry->translations)) { // Use the key (original text) as the key, and first translation as the value. $translations[$key] = $entry->translations[0]; } elseif (is_string($entry)) { // Sometimes entries might be stored as strings. $translations[$key] = $entry; } } } } return $translations; } }