'includes/dmm/option-groups/class-aieo-option-groups-schema.php', 'AIEO_Option_Groups_Store' => 'includes/dmm/option-groups/class-aieo-option-groups-store.php', 'AIEO_Option_Groups_Pricing' => 'includes/dmm/option-groups/class-aieo-option-groups-pricing.php', 'AIEO_Option_Groups_Frontend' => 'includes/dmm/option-groups/class-aieo-option-groups-frontend.php', ); foreach ( $deps as $class => $rel ) { if ( ! class_exists( $class ) ) { $abs = $dir . $rel; if ( file_exists( $abs ) ) { require_once $abs; } } } add_action( 'admin_init', array( __CLASS__, 'maybe_install' ) ); if ( class_exists( 'AIEO_Option_Groups_Frontend' ) ) { AIEO_Option_Groups_Frontend::instance()->init(); } add_action( 'wp_ajax_aieo_og_save_group', array( $this, 'ajax_save_group' ) ); add_action( 'wp_ajax_aieo_og_save_option', array( $this, 'ajax_save_option' ) ); add_action( 'wp_ajax_aieo_og_save_rule', array( $this, 'ajax_save_rule' ) ); add_action( 'wp_ajax_aieo_og_delete', array( $this, 'ajax_delete' ) ); add_action( 'wp_ajax_aieo_og_get_group', array( $this, 'ajax_get_group' ) ); add_action( 'wp_ajax_aieo_og_get_rules', array( $this, 'ajax_get_rules' ) ); 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' ) ); } public static function maybe_install() { if ( class_exists( 'AIEO_Option_Groups_Schema' ) ) { AIEO_Option_Groups_Schema::maybe_install(); } } public function enqueue_admin_assets() { $rel = 'assets/js/aieo-option-groups-admin.js'; $abs = ( defined( 'AIEO_DIR' ) ? AIEO_DIR : '' ) . $rel; $ver = ( $abs && file_exists( $abs ) ) ? (string) filemtime( $abs ) : ( defined( 'AIEO_VERSION' ) ? AIEO_VERSION : '1.0' ); wp_enqueue_script( 'aieo-option-groups-admin', plugins_url( $rel, AIEO_FILE ), array( 'jquery' ), $ver, true ); wp_localize_script( 'aieo-option-groups-admin', 'aieoOptionGroups', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE ), 'i18n' => array( 'saving' => __( 'Saving…', 'ai-eshop-optimizer' ), 'saved' => __( 'Saved.', 'ai-eshop-optimizer' ), 'confirmDel' => __( 'Delete this — including its options and rules?', 'ai-eshop-optimizer' ), 'name' => __( 'Give the group a name first.', 'ai-eshop-optimizer' ), 'newGroup' => __( 'New group', 'ai-eshop-optimizer' ), 'editGroup' => __( 'Edit', 'ai-eshop-optimizer' ), 'any' => __( 'any', 'ai-eshop-optimizer' ), 'allProducts' => __( 'All products', 'ai-eshop-optimizer' ), 'pickWhen' => __( 'Pick which option the condition is on.', 'ai-eshop-optimizer' ), ), ) ); } public function render_subtab() { if ( ! class_exists( 'AIEO_Option_Groups_Store' ) ) { echo '

' . esc_html__( 'Option Groups subsystem not loaded.', 'ai-eshop-optimizer' ) . '

'; return; } $this->render_template( 'option-groups.php', array( 'nonce' => wp_create_nonce( self::NONCE ), 'groups' => self::all_groups_summary(), ) ); } public static function all_groups_summary() { global $wpdb; $t = AIEO_Option_Groups_Schema::tables(); return (array) $wpdb->get_results( "SELECT g.id, g.name, g.select_type, g.min_select, g.max_select, g.included_free, g.free_policy, g.enabled, (SELECT COUNT(*) FROM {$t['options']} o WHERE o.group_id = g.id) AS option_count, (SELECT COUNT(*) FROM {$t['assignments']} a WHERE a.group_id = g.id) AS product_count FROM {$t['groups']} g ORDER BY g.name ASC", ARRAY_A ); } private function guard() { if ( ! check_ajax_referer( self::NONCE, 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-eshop-optimizer' ) ) ); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'Not allowed.', 'ai-eshop-optimizer' ) ) ); } } public function ajax_save_group() { $this->guard(); $data = array( 'id' => isset( $_POST['id'] ) ? (int) $_POST['id'] : 0, 'name' => isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '', 'select_type' => isset( $_POST['select_type'] ) ? sanitize_key( wp_unslash( $_POST['select_type'] ) ) : 'checkbox', 'min_select' => isset( $_POST['min_select'] ) ? (int) $_POST['min_select'] : 0, 'max_select' => isset( $_POST['max_select'] ) ? (int) $_POST['max_select'] : 0, 'included_free' => isset( $_POST['included_free'] ) ? (int) $_POST['included_free'] : 0, 'free_policy' => isset( $_POST['free_policy'] ) ? sanitize_key( wp_unslash( $_POST['free_policy'] ) ) : 'expensive_free', 'description' => isset( $_POST['description'] ) ? sanitize_text_field( wp_unslash( $_POST['description'] ) ) : '', ); if ( '' === $data['name'] ) { wp_send_json_error( array( 'message' => __( 'Name is required.', 'ai-eshop-optimizer' ) ) ); } $id = AIEO_Option_Groups_Store::upsert_group( $data ); wp_send_json_success( array( 'id' => $id ) ); } public function ajax_save_option() { $this->guard(); $gid = isset( $_POST['group_id'] ) ? (int) $_POST['group_id'] : 0; if ( $gid <= 0 && empty( $_POST['id'] ) ) { wp_send_json_error( array( 'message' => __( 'Missing group.', 'ai-eshop-optimizer' ) ) ); } $data = array( 'id' => isset( $_POST['id'] ) ? (int) $_POST['id'] : 0, 'group_id' => $gid, 'label' => isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : '', 'price_delta' => isset( $_POST['price_delta'] ) ? (float) $_POST['price_delta'] : 0, 'default_selected' => ! empty( $_POST['default_selected'] ) ? 1 : 0, 'exclude_from_allowance' => ! empty( $_POST['exclude_from_allowance'] ) ? 1 : 0, 'linked_product_id' => isset( $_POST['linked_product_id'] ) ? (int) $_POST['linked_product_id'] : 0, 'sku' => isset( $_POST['sku'] ) ? sanitize_text_field( wp_unslash( $_POST['sku'] ) ) : '', 'availability' => isset( $_POST['availability'] ) ? sanitize_key( wp_unslash( $_POST['availability'] ) ) : 'auto', ); if ( '' === $data['label'] ) { wp_send_json_error( array( 'message' => __( 'Option label is required.', 'ai-eshop-optimizer' ) ) ); } $id = AIEO_Option_Groups_Store::upsert_option( $data ); $gid = $gid > 0 ? $gid : (int) $this->option_group( (int) $id ); wp_send_json_success( array_merge( array( 'id' => $id ), $this->options_table( $gid ) ) ); } public function ajax_save_rule() { $this->guard(); $data = array( 'option_id' => isset( $_POST['option_id'] ) ? (int) $_POST['option_id'] : 0, 'scope_assignment_id' => isset( $_POST['scope_assignment_id'] ) ? (int) $_POST['scope_assignment_id'] : 0, 'when_group_id' => isset( $_POST['when_group_id'] ) ? (int) $_POST['when_group_id'] : 0, 'when_option_id' => isset( $_POST['when_option_id'] ) ? (int) $_POST['when_option_id'] : 0, 'price_delta' => isset( $_POST['price_delta'] ) ? (float) $_POST['price_delta'] : 0, ); if ( $data['option_id'] <= 0 ) { wp_send_json_error( array( 'message' => __( 'Missing option.', 'ai-eshop-optimizer' ) ) ); } $id = AIEO_Option_Groups_Store::upsert_rule( $data ); wp_send_json_success( array( 'id' => $id ) ); } public function ajax_delete() { $this->guard(); $which = isset( $_POST['which'] ) ? sanitize_key( wp_unslash( $_POST['which'] ) ) : ''; $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; if ( ! in_array( $which, array( 'groups', 'options', 'assignments', 'prices' ), true ) || $id <= 0 ) { wp_send_json_error( array( 'message' => __( 'Bad request.', 'ai-eshop-optimizer' ) ) ); } $gid = 'options' === $which ? $this->option_group( $id ) : 0; AIEO_Option_Groups_Store::delete( $which, $id ); wp_send_json_success( $gid > 0 ? $this->options_table( $gid ) : null ); } private function option_group( $oid ) { global $wpdb; $t = AIEO_Option_Groups_Schema::tables(); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$t['options']} WHERE id = %d", (int) $oid ) ); } private function drawn_table( $id, array $cols, array $rows, array $opts ) { if ( ! function_exists( 'rooster_admin_builders' ) || ! rooster_admin_builders() ) { return array( 'html' => '', 'icons' => '' ); } $html = AIEO_Table::local( $id, $cols, $rows, $opts + array( 'prefix' => $id . '_', 'sortable' => true ) ); return array( 'html' => $html, 'icons' => '' !== $html ? \Rooster\Admin\Ui::icons_of( $html ) : '' ); } private function options_table( $gid, $options = null ) { global $wpdb; if ( null === $options ) { $t = AIEO_Option_Groups_Schema::tables(); $options = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$t['options']} WHERE group_id = %d ORDER BY sort_order, label", (int) $gid ), ARRAY_A ); } $cur = function_exists( 'get_woocommerce_currency_symbol' ) ? html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' ) : ''; $rows = array(); foreach ( (array) $options as $o ) { $sku = (string) ( $o['sku'] ?? '' ); $avail = 'out_of_stock' === ( $o['availability'] ?? '' ) ? __( 'Sold out', 'ai-eshop-optimizer' ) : ( 'in_stock' === ( $o['availability'] ?? '' ) ? __( 'In stock', 'ai-eshop-optimizer' ) : __( 'Auto', 'ai-eshop-optimizer' ) . ( '' !== $sku ? ' · ' . $sku : '' ) ); $rows[] = array( 'label' => (string) $o['label'], 'price' => number_format( (float) ( $o['price_delta'] ?? 0 ), 2, '.', '' ), 'default' => (int) ( $o['default_selected'] ?? 0 ) ? 'yes' : 'no', 'premium' => (int) ( $o['exclude_from_allowance'] ?? 0 ) ? 'yes' : 'no', 'avail' => $avail, 'actions' => '' . esc_html__( 'Rules', 'ai-eshop-optimizer' ) . ' · ' . esc_html__( 'Delete', 'ai-eshop-optimizer' ) . '', '_id' => (int) $o['id'], '_data' => array( 'data-id' => (int) $o['id'] ), '_s' => array( 'default' => (int) ( $o['default_selected'] ?? 0 ), 'premium' => (int) ( $o['exclude_from_allowance'] ?? 0 ) ), ); } return $this->drawn_table( 'aieo-og-options-list', array( array( 'key' => 'label', 'label' => __( 'Label', 'ai-eshop-optimizer' ) ), array( 'key' => 'price', 'label' => sprintf( __( 'Price (%s)', 'ai-eshop-optimizer' ), $cur ), 'type' => 'num' ), array( 'key' => 'default', 'label' => __( 'Default', 'ai-eshop-optimizer' ), 'type' => 'boolean', 'sort' => 'number' ), array( 'key' => 'premium', 'label' => __( 'Always charged', 'ai-eshop-optimizer' ), 'type' => 'boolean', 'sort' => 'number' ), array( 'key' => 'avail', 'label' => __( 'Stock / availability', 'ai-eshop-optimizer' ) ), array( 'key' => 'actions', 'label' => '', 'type' => 'actions' ), ), $rows, array( 'size' => 0, 'empty' => esc_html__( 'No options yet: add the first one below.', 'ai-eshop-optimizer' ) ) ); } public function ajax_get_group() { $this->guard(); global $wpdb; $gid = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; $t = AIEO_Option_Groups_Schema::tables(); $group = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$t['groups']} WHERE id = %d", $gid ), ARRAY_A ); if ( ! $group ) { wp_send_json_error( array( 'message' => __( 'Not found.', 'ai-eshop-optimizer' ) ) ); } $options = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$t['options']} WHERE group_id = %d ORDER BY sort_order, label", $gid ), ARRAY_A ); $table = $this->options_table( $gid, $options ); wp_send_json_success( array( 'group' => $group, 'options' => $options, 'options_html' => $table['html'], 'options_icons' => $table['icons'] ) ); } public function ajax_get_rules() { $this->guard(); global $wpdb; $oid = isset( $_POST['option_id'] ) ? (int) $_POST['option_id'] : 0; $t = AIEO_Option_Groups_Schema::tables(); $opt = $wpdb->get_row( $wpdb->prepare( "SELECT id, group_id, label, price_delta FROM {$t['options']} WHERE id = %d", $oid ), ARRAY_A ); if ( ! $opt ) { wp_send_json_error( array( 'message' => __( 'Option not found.', 'ai-eshop-optimizer' ) ) ); } $gid = (int) $opt['group_id']; $cur = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : ''; $rules = (array) $wpdb->get_results( $wpdb->prepare( "SELECT id, scope_assignment_id, when_group_id, when_option_id, price_delta FROM {$t['prices']} WHERE option_id = %d ORDER BY id", $oid ), ARRAY_A ); foreach ( $rules as &$r ) { $bits = array(); if ( (int) $r['when_group_id'] > 0 && (int) $r['when_option_id'] > 0 ) { $gn = $wpdb->get_var( $wpdb->prepare( "SELECT name FROM {$t['groups']} WHERE id = %d", (int) $r['when_group_id'] ) ); $ol = $wpdb->get_var( $wpdb->prepare( "SELECT label FROM {$t['options']} WHERE id = %d", (int) $r['when_option_id'] ) ); $bits[] = sprintf( __( 'when %1$s = %2$s', 'ai-eshop-optimizer' ), $gn, $ol ); } if ( (int) $r['scope_assignment_id'] > 0 ) { $apid = (int) $wpdb->get_var( $wpdb->prepare( "SELECT product_id FROM {$t['assignments']} WHERE id = %d", (int) $r['scope_assignment_id'] ) ); $bits[] = sprintf( __( 'on %s', 'ai-eshop-optimizer' ), get_the_title( $apid ) ?: ( '#' . $apid ) ); } $cond = $bits ? implode( ', ', $bits ) : __( 'always', 'ai-eshop-optimizer' ); $r['desc'] = $cond . ' → ' . $cur . number_format_i18n( (float) $r['price_delta'], 2 ); } unset( $r ); $groups = (array) $wpdb->get_results( "SELECT id, name FROM {$t['groups']} ORDER BY name", ARRAY_A ); $assignments = array(); $arows = (array) $wpdb->get_results( $wpdb->prepare( "SELECT id, product_id FROM {$t['assignments']} WHERE group_id = %d", $gid ), ARRAY_A ); foreach ( $arows as $a ) { $assignments[] = array( 'assignment_id' => (int) $a['id'], 'name' => get_the_title( (int) $a['product_id'] ) ?: ( '#' . (int) $a['product_id'] ), ); } $list = array(); foreach ( $rules as $r ) { $list[] = array( 'rule' => (string) $r['desc'], 'actions' => '' . esc_html__( 'Delete', 'ai-eshop-optimizer' ) . '', '_id' => (int) $r['id'], '_data' => array( 'data-id' => (int) $r['id'] ), ); } $table = $this->drawn_table( 'aieo-og-rules-table', array( array( 'key' => 'rule', 'label' => __( 'Rule', 'ai-eshop-optimizer' ) ), array( 'key' => 'actions', 'label' => '', 'type' => 'actions' ), ), $list, array( 'size' => 0, 'empty' => esc_html__( 'No rules yet — the flat price applies.', 'ai-eshop-optimizer' ) ) ); wp_send_json_success( array( 'option' => array( 'id' => (int) $opt['id'], 'label' => $opt['label'], 'base' => (float) $opt['price_delta'] ), 'rules' => $rules, 'html' => $table['html'], 'icons' => $table['icons'], 'groups' => $groups, 'assignments' => $assignments, ) ); } public function add_product_data_tab( $tabs ) { $tabs['aieo_option_groups'] = array( 'label' => __( 'Options', 'ai-eshop-optimizer' ), 'target' => 'aieo_option_groups_product_data', 'class' => array(), 'priority' => 65, ); return $tabs; } public function render_product_data_panel() { global $post; if ( ! $post || ! class_exists( 'AIEO_Option_Groups_Schema' ) ) { return; } $product_id = (int) $post->ID; $all = self::all_groups_summary(); $assigned = self::assigned_group_ids( $product_id ); $lib_url = admin_url( 'admin.php?page=ai-eshop-dmm&dmm_tab=option_groups' ); echo ''; } public function save_product_meta( $product_id ) { $product_id = (int) $product_id; if ( ! isset( $_POST['aieo_og_product_nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['aieo_og_product_nonce'] ) ), 'aieo_og_product_' . $product_id ) ) { return; } if ( ! current_user_can( 'edit_product', $product_id ) || ! class_exists( 'AIEO_Option_Groups_Store' ) ) { return; } $want = isset( $_POST['aieo_og_groups'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['aieo_og_groups'] ) ) : array(); $want = array_values( array_unique( array_filter( $want ) ) ); global $wpdb; $t = AIEO_Option_Groups_Schema::tables(); $have = self::assigned_group_ids( $product_id ); $order = 0; foreach ( $want as $gid ) { AIEO_Option_Groups_Store::assign_group( $product_id, $gid, $order ); $order++; } foreach ( array_diff( $have, $want ) as $gid ) { $wpdb->delete( $t['assignments'], array( 'product_id' => $product_id, 'group_id' => (int) $gid ) ); } } public static function assigned_group_ids( $product_id ) { global $wpdb; $t = AIEO_Option_Groups_Schema::tables(); return array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare( "SELECT group_id FROM {$t['assignments']} WHERE product_id = %d ORDER BY sort_order ASC", (int) $product_id ) ) ); } }