false, 'label' => '' ); } return $d; } public static function get_slots_config( $raw = false ) { $stored = get_option( self::OPTION_KEY, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } $out = self::default_config(); foreach ( $out as $i => $def ) { if ( isset( $stored[ $i ] ) && is_array( $stored[ $i ] ) ) { $out[ $i ] = array( 'enabled' => ! empty( $stored[ $i ]['enabled'] ), 'label' => isset( $stored[ $i ]['label'] ) ? (string) $stored[ $i ]['label'] : $def['label'], ); } if ( ! $raw && '' === trim( (string) $out[ $i ]['label'] ) ) { $out[ $i ]['label'] = self::slot_label( $i ); } } return $out; } public static function get_enabled_slots() { $out = array(); foreach ( self::get_slots_config() as $i => $s ) { if ( ! empty( $s['enabled'] ) && '' !== trim( (string) $s['label'] ) ) { $out[ $i ] = $s; } } return $out; } private static function table() { global $wpdb; return $wpdb->prefix . 'aieo_dmm_custom_tabs'; } private static function table_ok() { global $wpdb; static $ok = null; if ( null === $ok ) { $t = self::table(); $ok = ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t ); } return $ok; } public static function get_all_content( $product_id ) { $product_id = (int) $product_id; if ( $product_id <= 0 || ! self::table_ok() ) { return array(); } global $wpdb; $t = self::table(); $rows = (array) $wpdb->get_results( $wpdb->prepare( "SELECT slot_index, content FROM `{$t}` WHERE product_id = %d", $product_id ), ARRAY_A ); $out = array(); foreach ( $rows as $r ) { $out[ (int) $r['slot_index'] ] = (string) $r['content']; } return $out; } public static function save_content( $product_id, $slot, $html ) { $product_id = (int) $product_id; $slot = (int) $slot; if ( $product_id <= 0 || $slot < 1 || $slot > self::SLOT_COUNT || ! self::table_ok() ) { return; } global $wpdb; $t = self::table(); $html = (string) $html; if ( '' === trim( $html ) ) { $wpdb->delete( $t, array( 'product_id' => $product_id, 'slot_index' => $slot ), array( '%d', '%d' ) ); return; } $wpdb->replace( $t, array( 'product_id' => $product_id, 'slot_index' => $slot, 'content' => $html, 'updated_at' => current_time( 'mysql', true ), ), array( '%d', '%d', '%s', '%s' ) ); } public function register_hooks() { add_action( 'wp_ajax_aieo_dmm_custom_tabs_save', array( $this, 'ajax_save' ) ); add_action( 'wp_ajax_aieo_dmm_custom_tabs_migrate', array( $this, 'ajax_migrate' ) ); add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_data_tab' ) ); add_action( 'woocommerce_product_data_panels', array( $this, 'render_product_data_panel' ) ); add_action( 'woocommerce_process_product_meta', array( $this, 'save_product_meta' ) ); add_filter( 'woocommerce_product_tabs', array( $this, 'add_tabs' ) ); add_action( 'init', array( $this, 'maybe_replace_legacy_tabs' ), 20 ); } public function maybe_replace_legacy_tabs() { if ( empty( self::get_enabled_slots() ) ) { return; } if ( function_exists( 'custom_pods_product_tabs' ) ) { remove_filter( 'woocommerce_product_tabs', 'custom_pods_product_tabs' ); remove_filter( 'woocommerce_product_tabs', 'custom_pods_product_tabs', 10 ); } } public function add_tabs( $tabs ) { global $product; if ( ! $product || ! is_a( $product, 'WC_Product' ) ) { return $tabs; } $content = self::get_all_content( $product->get_id() ); foreach ( self::get_enabled_slots() as $i => $slot ) { $html = isset( $content[ $i ] ) ? trim( (string) $content[ $i ] ) : ''; if ( '' === $html ) { continue; } $tabs[ 'aieo_custom_tab_' . (int) $i ] = array( 'title' => $slot['label'], 'priority' => 50 + (int) $i, 'callback' => static function () use ( $html ) { echo wpautop( wp_kses_post( $html ) ); }, ); } return $tabs; } public function add_product_data_tab( $tabs ) { $tabs['aieo_custom_tabs'] = array( 'label' => __( 'Custom Tabs', 'ai-eshop-optimizer' ), 'target' => 'aieo_custom_tabs_product_data', 'priority' => 66, 'class' => array(), ); return $tabs; } public function render_product_data_panel() { global $post; echo ''; return; } $content = self::get_all_content( (int) $post->ID ); echo '
'; foreach ( $enabled as $i => $slot ) { echo '

' . esc_html( $slot['label'] ) . '

'; wp_editor( isset( $content[ $i ] ) ? (string) $content[ $i ] : '', 'aieo_custom_tab_' . (int) $i, array( 'textarea_name' => 'aieo_custom_tab_' . (int) $i, 'media_buttons' => true, 'tinymce' => true, 'quicktags' => true, 'textarea_rows' => 8, ) ); } echo '
'; } public function save_product_meta( $product_id ) { $product_id = (int) $product_id; if ( ! current_user_can( 'edit_product', $product_id ) ) { return; } foreach ( self::get_enabled_slots() as $i => $slot ) { $key = 'aieo_custom_tab_' . (int) $i; if ( ! isset( $_POST[ $key ] ) ) { continue; } $html = wp_kses_post( wp_unslash( $_POST[ $key ] ) ); self::save_content( $product_id, (int) $i, $html ); } } private static function pods_rows_count() { global $wpdb; return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key IN ('usage','specification') AND TRIM(meta_value) <> ''" ); } public function render_subtab() { $slots = self::get_slots_config(); $nonce = wp_create_nonce( AIEO_DMM::NONCE_ACTION ); $ajax = admin_url( 'admin-ajax.php' ); global $wpdb; $t = self::table(); $table_ok = ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t ); $row_count = $table_ok ? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$t}`" ) : 0; $migrated = (int) get_option( self::MIGRATED_OPTION, 0 ); $pods_rows = $table_ok ? self::pods_rows_count() : 0; rooster_admin_band_open( array( 'cols' => 2 ) ); $form_id = 'aieo-card-custom-tabs-overview-form'; list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'custom-tabs-overview', 'title' => __( 'Tab names and on/off', 'ai-eshop-optimizer' ), 'description' => __( 'Up to five extra product tabs with rich-text content per product. Enable + rename the ones you want, then edit each product\'s content in the "Custom Tabs" panel on the product edit screen. A tab with no content for a given product is hidden automatically.', 'ai-eshop-optimizer' ), 'footer' => class_exists( '\Rooster\Admin\States' ) ? \Rooster\Admin\States::save_button( $form_id ) : '', ) ); echo $open; $fields = '
'; foreach ( $slots as $i => $s ) { $i = (int) $i; $tab_label = sprintf( __( 'Tab %d', 'ai-eshop-optimizer' ), $i ); $fields .= \Rooster\Admin\Ui::field_row( array( 'id' => 'aieo-ct-tab-' . $i, 'label' => $tab_label, 'parts' => array( array( 'label' => __( 'Enabled', 'ai-eshop-optimizer' ), 'id' => 'aieo-ct-enabled-' . $i, 'control' => \Rooster\Admin\Ui::toggle( 'enabled_' . $i, ! empty( $s['enabled'] ), array( 'id' => 'aieo-ct-enabled-' . $i, 'hidden' => false, 'label' => __( 'On', 'ai-eshop-optimizer' ), 'label_hidden' => true ) ), ), array( 'label' => __( 'Tab name', 'ai-eshop-optimizer' ), 'id' => 'aieo-ct-label-' . $i, 'control' => \Rooster\Admin\Ui::input( 'label_' . $i, (string) $s['label'], array( 'id' => 'aieo-ct-label-' . $i, 'medium' => true, 'placeholder' => $tab_label ) ), ), ), 'help' => __( 'It shows on a product\'s page while it is on and named, and that product has content for it.', 'ai-eshop-optimizer' ), ) ); } $fields .= '
'; echo AIEO_DMM_UI::card_form( 'aieo_dmm_custom_tabs_save', 'custom_tabs', $form_id, $fields ); echo $close; list( $open, $close ) = AIEO_DMM_UI::card( array( 'anchor' => 'custom-tabs-migrate-from-pods', 'icon' => 'import', 'title' => __( 'Migrate from Pods', 'ai-eshop-optimizer' ), 'description' => __( 'Copies the legacy Pods fields into Custom Tabs: usage → Tab 1, specification → Tab 2 (and enables + names those two if they are still at their defaults). Re-running is safe — it overwrites, never duplicates.', 'ai-eshop-optimizer' ), ) ); echo $open; $storage = $table_ok ? \Rooster\Admin\Ui::badge( sprintf( __( '%d rows', 'ai-eshop-optimizer' ), $row_count ), 'success' ) : \Rooster\Admin\Ui::badge( __( 'table missing', 'ai-eshop-optimizer' ), 'destructive' ); $storage_row = \Rooster\Admin\Ui::field_row( array( 'id' => 'aieo-ct-storage', 'group' => true, 'label' => __( 'Storage', 'ai-eshop-optimizer' ), 'control' => $storage, 'help' => __( 'One row for each product and tab with content.', 'ai-eshop-optimizer' ) ) ); echo '
' . $storage_row . '
'; $ask_attrs = array(); $dialog = ''; if ( $pods_rows > 0 ) { $ask_attrs = \Rooster\Admin\States::confirm_attrs( 'aieo-ct-migrate-dialog' ); $dialog = \Rooster\Admin\States::confirm( 'aieo-ct-migrate-dialog', 'replace', __( 'Overwrite the tabs with the Pods fields?', 'ai-eshop-optimizer' ), '

' . esc_html( sprintf( _n( '%s Pods field will be copied into Custom Tabs (usage into Tab 1, specification into Tab 2). Content those tabs already hold for the same products is overwritten.', '%s Pods fields will be copied into Custom Tabs (usage into Tab 1, specification into Tab 2). Content those tabs already hold for the same products is overwritten.', $pods_rows, 'ai-eshop-optimizer' ), number_format_i18n( $pods_rows ) ) ) . '

', array( 'confirm' => __( 'Migrate from Pods', 'ai-eshop-optimizer' ) ) ); } $job = \Rooster\Admin\States::job( array( 'id' => 'aieo-ct-job', 'state' => $migrated ? 'idle' : 'not-run', 'run' => __( 'Migrate from Pods (usage / specification)', 'ai-eshop-optimizer' ), 'run_attrs' => array( 'id' => 'aieo-ct-migrate' ) + $ask_attrs, 'primary' => false, 'result' => $migrated ? __( 'Last migration', 'ai-eshop-optimizer' ) : '', 'last' => $migrated ? $migrated : 0, ) ); echo $job; echo $dialog; echo $close; rooster_admin_band_close(); $migrate_js = ''; echo $migrate_js; } public function ajax_save() { AIEO_DMM::verify_ajax_request(); $clean = array(); for ( $i = 1; $i <= self::SLOT_COUNT; $i++ ) { $clean[ $i ] = array( 'enabled' => ! empty( $_POST[ 'enabled_' . $i ] ), 'label' => isset( $_POST[ 'label_' . $i ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'label_' . $i ] ) ) : '', ); } update_option( self::OPTION_KEY, $clean, false ); wp_send_json_success( array( 'message' => __( 'Custom Tabs saved.', 'ai-eshop-optimizer' ) ) ); } public function ajax_migrate() { AIEO_DMM::verify_ajax_request(); @ob_end_clean(); $stats = self::run_migration(); wp_send_json_success( array( 'stats' => $stats, 'migrated_at' => time() ) ); } public static function run_migration() { global $wpdb; $stats = array( 'processed' => 0, 'migrated' => 0, 'skipped' => 0 ); if ( ! self::table_ok() ) { return $stats; } $map = array( 'usage' => 1, 'specification' => 2 ); $rows = (array) $wpdb->get_results( "SELECT post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE meta_key IN ('usage','specification') AND TRIM(meta_value) <> ''", ARRAY_A ); foreach ( $rows as $r ) { $stats['processed']++; $slot = isset( $map[ $r['meta_key'] ] ) ? (int) $map[ $r['meta_key'] ] : 0; if ( ! $slot ) { $stats['skipped']++; continue; } self::save_content( (int) $r['post_id'], $slot, wp_kses_post( (string) $r['meta_value'] ) ); $stats['migrated']++; } $cfg = self::get_slots_config( true ); $changed = false; $seed = array( 1 => 'Εφαρμογή', 2 => 'Συστατικά' ); foreach ( $seed as $i => $greek ) { if ( empty( $cfg[ $i ]['enabled'] ) ) { $cfg[ $i ]['enabled'] = true; if ( '' === trim( (string) $cfg[ $i ]['label'] ) || in_array( $cfg[ $i ]['label'], array( 'Application', 'Ingredients' ), true ) ) { $cfg[ $i ]['label'] = $greek; } $changed = true; } } if ( $changed ) { update_option( self::OPTION_KEY, $cfg, false ); } update_option( self::MIGRATED_OPTION, time(), false ); return $stats; } }