array( 'below_favourite' => __('Below favourite (right column)', 'ai-eshop-optimizer'), 'top_left' => __('Top-left over image', 'ai-eshop-optimizer'), 'top_right' => __('Top-right over image', 'ai-eshop-optimizer'), 'inline' => __('Inline above title', 'ai-eshop-optimizer'), ), 'pdp' => array( 'top_left' => __('Top-left over gallery', 'ai-eshop-optimizer'), 'top_right' => __('Top-right over gallery', 'ai-eshop-optimizer'), 'inline' => __('Inline above title (right column)', 'ai-eshop-optimizer'), ), 'recos' => array( 'inline' => __('Inline above title', 'ai-eshop-optimizer'), 'top_left' => __('Top-left over image', 'ai-eshop-optimizer'), 'top_right' => __('Top-right over image', 'ai-eshop-optimizer'), ), 'side_cart' => array( 'inline' => __('Inline above title', 'ai-eshop-optimizer'), 'top_left' => __('Top-left over thumb', 'ai-eshop-optimizer'), ), ); } public static function shape_options() { return array( 'chip' => __('Chip (rounded rectangle)', 'ai-eshop-optimizer'), 'circle' => __('Circle (round, icon-only friendly)', 'ai-eshop-optimizer'), ); } public static function default_location($surface) { $opts = self::location_options(); if (!isset($opts[$surface]) || empty($opts[$surface])) { return ''; } return (string) array_key_first($opts[$surface]); } public static function default_shape() { $opts = self::shape_options(); return $opts ? (string) array_key_first($opts) : 'chip'; } public static function migrate_legacy_placement($legacy) { $legacy = (string) $legacy; if ($legacy === '') { return null; } if ($legacy === 'below_favourite') { return array('location' => 'below_favourite', 'shape' => 'circle'); } if (strpos($legacy, 'top_left_') === 0) { return array('location' => 'top_left', 'shape' => 'chip'); } if (strpos($legacy, 'top_right_') === 0) { return array('location' => 'top_right', 'shape' => 'chip'); } if (strpos($legacy, 'inline') === 0) { return array('location' => 'inline', 'shape' => 'chip'); } return null; } public static function defaults() { return array( 'version' => self::SETTINGS_VERSION, 'hide_wc_onsale' => true, 'sizes' => array( 'archive' => array('desktop' => 11, 'mobile' => 10), 'pdp' => array('desktop' => 13, 'mobile' => 11), 'recos' => array('desktop' => 10, 'mobile' => 9), 'side_cart' => array('desktop' => 10, 'mobile' => 9), ), 'placements' => array( 'archive' => array('location' => 'below_favourite', 'shape' => 'circle'), 'pdp' => array('location' => 'top_left', 'shape' => 'chip'), 'recos' => array('location' => 'inline', 'shape' => 'chip'), 'side_cart' => array('location' => 'top_left', 'shape' => 'chip'), ), 'families' => array( 'gift' => self::default_family('', '#0a7d3a', '#ffffff', 5, 'π'), 'monthly_offer' => self::default_family('', '#F46767', '#ffffff', 10), 'sale' => self::default_family('', '#dc2626', '#ffffff', 15), 'pro' => self::default_family('PRO', '#1f3b8b', '#ffffff', 20, 'β '), 'new' => self::default_family('', '#1a1a1a', '#ffffff', 30), ), ); } private static function i18n_family_labels() { return array( 'sale' => __('SALE', 'ai-eshop-optimizer'), 'monthly_offer' => __('MONTHLY OFFER', 'ai-eshop-optimizer'), 'new' => __('NEW', 'ai-eshop-optimizer'), 'gift' => __('+ Free gift', 'ai-eshop-optimizer'), ); } private static function fill_i18n_defaults(array $out) { if (!did_action('init')) { return $out; } if (!isset($out['families']) || !is_array($out['families'])) { return $out; } foreach (self::i18n_family_labels() as $key => $label) { if (!isset($out['families'][$key]) || !is_array($out['families'][$key])) { continue; } if ('' === trim((string) ($out['families'][$key]['label'] ?? ''))) { $out['families'][$key]['label'] = $label; } } return $out; } private static function default_family($label, $bg, $fg, $order, $icon = '') { return array( 'enabled' => true, 'label' => (string) $label, 'icon' => (string) $icon, 'icon_image' => 0, 'icon_image_url' => '', 'order' => (int) $order, 'bg' => (string) $bg, 'fg' => (string) $fg, 'surfaces' => array( 'archive' => true, 'pdp' => true, 'recos' => true, 'side_cart' => false, ), ); } public static function get_settings($raw = false) { $stored = get_option(self::OPTION_KEY, array()); if (!is_array($stored)) { $stored = array(); } $defaults = self::defaults(); $out = array_merge($defaults, $stored); $out['sizes'] = array(); foreach ($defaults['sizes'] as $surface => $def) { $stored_val = isset($stored['sizes'][$surface]) ? $stored['sizes'][$surface] : null; if (is_int($stored_val) || (is_string($stored_val) && ctype_digit($stored_val))) { $out['sizes'][$surface] = array( 'desktop' => (int) $stored_val, 'mobile' => max(8, (int) round((int) $stored_val * 0.9)), ); } elseif (is_array($stored_val)) { $out['sizes'][$surface] = array( 'desktop' => isset($stored_val['desktop']) ? (int) $stored_val['desktop'] : (int) $def['desktop'], 'mobile' => isset($stored_val['mobile']) ? (int) $stored_val['mobile'] : (int) $def['mobile'], ); } else { $out['sizes'][$surface] = $def; } } $out['placements'] = $defaults['placements']; $stored_placements = isset($stored['placements']) && is_array($stored['placements']) ? $stored['placements'] : array(); foreach ($defaults['placements'] as $surface => $def_pl) { $sp = isset($stored_placements[$surface]) ? $stored_placements[$surface] : null; if (is_string($sp)) { $migrated = self::migrate_legacy_placement($sp); $out['placements'][$surface] = $migrated ? $migrated : $def_pl; } elseif (is_array($sp)) { $out['placements'][$surface] = array( 'location' => isset($sp['location']) ? (string) $sp['location'] : $def_pl['location'], 'shape' => isset($sp['shape']) ? (string) $sp['shape'] : $def_pl['shape'], ); } } if (!isset($stored_placements['archive']) && isset($stored['archive_layout']) && is_string($stored['archive_layout'])) { $migrated = self::migrate_legacy_placement($stored['archive_layout']); if ($migrated) { $out['placements']['archive'] = $migrated; } } $valid_locations = self::location_options(); $valid_shapes = self::shape_options(); foreach ($out['placements'] as $surface => $pl) { if (!isset($valid_locations[$surface][$pl['location']])) { $out['placements'][$surface]['location'] = self::default_location($surface); } if (!isset($valid_shapes[$pl['shape']])) { $out['placements'][$surface]['shape'] = self::default_shape(); } } $out['families'] = isset($stored['families']) && is_array($stored['families']) ? $stored['families'] : array(); foreach ($defaults['families'] as $key => $family_default) { $stored_family = isset($out['families'][$key]) && is_array($out['families'][$key]) ? $out['families'][$key] : array(); $merged = array_merge($family_default, $stored_family); $merged['surfaces'] = array_merge( $family_default['surfaces'], is_array($stored_family['surfaces'] ?? null) ? $stored_family['surfaces'] : array() ); $out['families'][$key] = $merged; } return $raw ? $out : self::fill_i18n_defaults($out); } public static function get_family($key) { $s = self::get_settings(); return isset($s['families'][$key]) ? $s['families'][$key] : null; } public static function family_enabled_for_surface($key, $surface) { $f = self::get_family($key); if (!$f || empty($f['enabled'])) { return false; } return !empty($f['surfaces'][$surface]); } public static function maybe_migrate() { $existing = get_option(self::OPTION_KEY, null); if (is_array($existing) && isset($existing['version']) && (int) $existing['version'] >= self::SETTINGS_VERSION) { return; } $unified = self::build_from_legacy(); update_option(self::OPTION_KEY, $unified, false); } public static function maybe_migrate_order() { if (get_option('aieo_badges_order_v2')) { return; } $stored = get_option(self::OPTION_KEY, null); if (!is_array($stored) || empty($stored['families']) || !is_array($stored['families'])) { update_option('aieo_badges_order_v2', 1, false); return; } $order = static function ($key) use ($stored) { return isset($stored['families'][$key]['order']) ? (int) $stored['families'][$key]['order'] : null; }; $untouched = 10 === $order('sale') && 15 === $order('monthly_offer') && 20 === $order('pro') && 30 === $order('new') && in_array($order('gift'), array(40, 5), true); if ($untouched) { $defaults = self::defaults(); foreach ($defaults['families'] as $key => $family) { if (isset($stored['families'][$key]) && is_array($stored['families'][$key])) { $stored['families'][$key]['order'] = (int) $family['order']; } } update_option(self::OPTION_KEY, $stored, false); } update_option('aieo_badges_order_v2', 1, false); } public static function build_from_legacy() { $unified = self::defaults(); $pls = get_option('aieo_dmm_product_labels_settings', array()); if (!is_array($pls)) { $pls = array(); } $legacy_pls = get_option('om_ppe_product-labels', array()); if (!is_array($legacy_pls)) { $legacy_pls = array(); } $pls = array_merge($legacy_pls, $pls); if (!empty($pls)) { $rule = isset($pls['display_rules']) ? $pls['display_rules'] : 'products_on_sale'; $family_key = ($rule === 'new_products') ? 'new' : 'sale'; $unified['families'][$family_key]['enabled'] = !empty($pls['enabled']); if (!empty($pls['label_text'])) { $unified['families'][$family_key]['label'] = (string) $pls['label_text']; } if (!empty($pls['background_color'])) { $unified['families'][$family_key]['bg'] = (string) $pls['background_color']; } if (!empty($pls['text_color'])) { $unified['families'][$family_key]['fg'] = (string) $pls['text_color']; } } if (class_exists('AIEO_DMM_Settings')) { $ml = (string) AIEO_DMM_Settings::get('monthly_offer_label', ''); if ($ml !== '') { $unified['families']['monthly_offer']['label'] = $ml; } $nl = (string) AIEO_DMM_Settings::get('new_product_label', ''); if ($nl !== '') { $unified['families']['new']['label'] = $nl; } } $rp = get_option('aieo_dmm_role_pricing_settings', array()); if (is_array($rp)) { if (!empty($rp['pro_badge_label'])) { $unified['families']['pro']['label'] = (string) $rp['pro_badge_label']; } if (!empty($rp['pro_badge_bg_color'])) { $unified['families']['pro']['bg'] = (string) $rp['pro_badge_bg_color']; } if (!empty($rp['pro_badge_text_color'])) { $unified['families']['pro']['fg'] = (string) $rp['pro_badge_text_color']; } if (!empty($rp['pro_badge_tooltip'])) { $unified['families']['pro']['tooltip'] = (string) $rp['pro_badge_tooltip']; } } $arch_pos = class_exists('AIEO_DMM_Settings') ? (string) AIEO_DMM_Settings::get('archive_badge_position', 'image_top_left') : 'image_top_left'; $arch_placement = ($arch_pos === 'below_favourite') ? 'below_favourite' : 'top_left_over_image'; $unified['placements']['archive'] = $arch_placement; return $unified; } public static function sanitize(array $input) { $defaults = self::defaults(); $out = $defaults; $out['version'] = self::SETTINGS_VERSION; $out['hide_wc_onsale'] = !empty($input['hide_wc_onsale']); $valid_families = array_keys($defaults['families']); $valid_surfaces = array('archive', 'pdp', 'recos', 'side_cart'); $location_opts = self::location_options(); $shape_opts = self::shape_options(); $out['sizes'] = array(); foreach ($valid_surfaces as $s) { $def_d = (int) $defaults['sizes'][$s]['desktop']; $def_m = (int) $defaults['sizes'][$s]['mobile']; $raw = isset($input['sizes'][$s]) ? $input['sizes'][$s] : array(); if (is_int($raw) || (is_string($raw) && ctype_digit($raw))) { $d = max(8, min(24, (int) $raw)); $m = max(8, min(24, (int) round($d * 0.9))); } else { $d = isset($raw['desktop']) ? max(8, min(24, (int) $raw['desktop'])) : $def_d; $m = isset($raw['mobile']) ? max(8, min(24, (int) $raw['mobile'])) : $def_m; } $out['sizes'][$s] = array('desktop' => $d, 'mobile' => $m); } $out['placements'] = array(); foreach ($valid_surfaces as $s) { $raw_loc = isset($input['placements'][$s]['location']) ? (string) $input['placements'][$s]['location'] : ''; $raw_shape = isset($input['placements'][$s]['shape']) ? (string) $input['placements'][$s]['shape'] : ''; $out['placements'][$s] = array( 'location' => isset($location_opts[$s][$raw_loc]) ? $raw_loc : self::default_location($s), 'shape' => isset($shape_opts[$raw_shape]) ? $raw_shape : self::default_shape(), ); } $out['families'] = array(); foreach ($valid_families as $key) { $src = isset($input['families'][$key]) && is_array($input['families'][$key]) ? $input['families'][$key] : array(); $def = $defaults['families'][$key]; $img_id = isset($src['icon_image']) ? (int) $src['icon_image'] : 0; if ($img_id > 0) { $img_url = wp_get_attachment_url($img_id); $img_url = $img_url ? esc_url_raw($img_url) : ''; } else { $img_id = 0; $img_url = ''; } $f = array( 'enabled' => !empty($src['enabled']), 'label' => isset($src['label']) ? sanitize_text_field((string) $src['label']) : $def['label'], 'icon' => isset($src['icon']) ? sanitize_text_field((string) $src['icon']) : $def['icon'], 'icon_image' => $img_id, 'icon_image_url' => $img_url, 'order' => isset($src['order']) ? max(0, min(999, (int) $src['order'])) : $def['order'], 'bg' => isset($src['bg']) ? (sanitize_hex_color((string) $src['bg']) ?: $def['bg']) : $def['bg'], 'fg' => isset($src['fg']) ? (sanitize_hex_color((string) $src['fg']) ?: $def['fg']) : $def['fg'], ); $f['surfaces'] = array(); foreach ($valid_surfaces as $s) { $f['surfaces'][$s] = isset($src['surfaces'][$s]) ? !empty($src['surfaces'][$s]) : !empty($def['surfaces'][$s]); } if (isset($src['tooltip'])) { $f['tooltip'] = sanitize_text_field((string) $src['tooltip']); } $out['families'][$key] = $f; } return $out; } public function ajax_save() { AIEO_DMM::verify_ajax_request(); $raw = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : array(); if (!is_array($raw)) { $decoded = json_decode((string) $raw, true); $raw = is_array($decoded) ? $decoded : array(); } $clean = self::sanitize($raw); update_option(self::OPTION_KEY, $clean, false); if (array_key_exists('storefront', $raw) && class_exists('AIEO_DMM_Badge_Renderer')) { update_option(AIEO_DMM_Badge_Renderer::FLAG_OPTION, !empty($raw['storefront']) ? 1 : 0); } wp_send_json_success(array( 'message' => __('Badge settings saved.', 'ai-eshop-optimizer'), 'settings' => $clean, )); } public function render_subtab() { $s = self::get_settings(true); $i18n_labels = self::i18n_family_labels(); $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION); $ajax = admin_url('admin-ajax.php'); wp_enqueue_media(); $new_days = (int) AIEO_DMM_Settings::get('new_product_days', 60); $aieo_bd_new_form = 'aieo-card-badges-new-product-threshold-form'; list($aieo_bd_open, $aieo_bd_close) = AIEO_DMM_UI::card(array( 'anchor' => 'badges-new-product-threshold', 'icon' => 'calendar-days', 'title' => __('NEW badge validity in days', 'ai-eshop-optimizer'), 'description' => __('Number of days after publication during which a product counts as "new". Drives the NEW badge and the AI-chat recency boost. Set 0 to disable the NEW badge.', 'ai-eshop-optimizer'), 'footer' => class_exists('\Rooster\Admin\States') ? \Rooster\Admin\States::save_button($aieo_bd_new_form) : '', )); echo $aieo_bd_open; $aieo_bd_new_row = \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-badges-new-days', 'label' => __('Days since publication', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('new_product_days', (string) $new_days, array('id' => 'aieo-badges-new-days', 'type' => 'number', 'xs' => true, 'min' => 0, 'max' => 3650, 'step' => 1, 'unit' => __('days', 'ai-eshop-optimizer'))), )); echo AIEO_DMM_UI::card_form('aieo_dmm_save_badge_settings', 'badges_new_days', $aieo_bd_new_form, '
' . esc_html($aieo_bd_css) . '' ); echo $aieo_bd_close; ?>