'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 '';
echo '
';
echo '
';
echo '' . sprintf(
wp_kses_post( __( 'Tick the option groups that apply to this product. Manage the groups themselves in %s.', 'ai-eshop-optimizer' ) ),
'' . esc_html__( 'Product Options', 'ai-eshop-optimizer' ) . ''
) . '';
if ( empty( $all ) ) {
echo '' . esc_html__( 'No option groups yet — create one in the library first.', 'ai-eshop-optimizer' ) . '';
} else {
wp_nonce_field( 'aieo_og_product_' . $product_id, 'aieo_og_product_nonce' );
echo '
';
}
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
) ) );
}
}