load_services(); } private function load_services() { $base = AIEO_DIR . 'includes/shipping-rates/'; foreach (array( 'class-aieo-trs-rules.php', 'class-aieo-trs-validator.php', 'class-aieo-trs-csv.php', 'class-aieo-trs-zone-admin.php', 'class-aieo-trs-migrator.php', ) as $f) { $path = $base . $f; if (file_exists($path)) { require_once $path; } } if (class_exists('AIEO_TRS_Zone_Admin')) { AIEO_TRS_Zone_Admin::register(); } if (class_exists('AIEO_TRS_Migrator')) { AIEO_TRS_Migrator::register(); } } public function register_hooks() { } public function get_tab_arg() { return 'trs_section'; } public function get_tabs() { $tabs = array( 'rules' => array('label' => __('Rules', 'ai-eshop-optimizer'), 'icon' => 'list-checks'), 'csv' => array('label' => __('Import / Export', 'ai-eshop-optimizer'), 'icon' => 'table'), ); if (class_exists('AIEO_TRS_Migrator') && AIEO_TRS_Migrator::has_legacy_table()) { $tabs['migrator'] = array('label' => __('Migrate from legacy', 'ai-eshop-optimizer'), 'icon' => 'import'); } return $tabs; } public function enqueue_admin_assets() { $css = 'assets/css/aieo-trs-admin.css'; wp_enqueue_style('aieo-trs-admin', AIEO_URI . $css, array(), file_exists(AIEO_DIR . $css) ? (string) filemtime(AIEO_DIR . $css) : '1'); } public function render_subtab() { $tabs = $this->get_tabs(); $section = isset($_GET['trs_section']) ? sanitize_key((string) $_GET['trs_section']) : 'rules'; if (!isset($tabs[$section])) { $section = 'rules'; } $base_url = admin_url('admin.php?page=' . AIEO_DMM::MENU_SLUG . '&dmm_tab=shipping_rates'); AIEO_DMM_UI::tab_strip($this->get_id(), $tabs, $section, array( 'arg' => $this->get_tab_arg(), 'wrapper' => 'div', 'margin' => '14px', )); switch ($section) { case 'rules': $this->render_rules(); break; case 'csv': $this->render_csv(); break; case 'migrator': $this->render_migrator(); break; } } private function discover_instances() { $out = array(); if (!class_exists('WC_Shipping_Zones')) { return $out; } $zones = WC_Shipping_Zones::get_zones(); $rest = WC_Shipping_Zones::get_zone(0); if ($rest) { $zones[0] = array( 'zone_name' => __('Rest of the World', 'ai-eshop-optimizer'), 'shipping_methods' => $rest->get_shipping_methods(false), ); } foreach ($zones as $zone_id => $zone) { $methods = isset($zone['shipping_methods']) ? $zone['shipping_methods'] : array(); foreach ($methods as $m) { $mid = method_exists($m, 'get_id') ? $m->id : (string) ($m->id ?? ''); $iid = (int) (method_exists($m, 'get_instance_id') ? $m->get_instance_id() : ($m->instance_id ?? 0)); if ($iid <= 0) { continue; } $title = method_exists($m, 'get_title') ? (string) $m->get_title() : (string) ($m->title ?? ''); if ($title === '') { $title = (string) ($m->method_title ?? __('Shipping', 'ai-eshop-optimizer')); } $is_table_rate = ($mid === 'aieo_table_rate' || $mid === 'table_rate'); $is_enabled = method_exists($m, 'is_enabled') ? (bool) $m->is_enabled() : true; $out[] = array( 'instance_id' => $iid, 'zone_id' => (int) $zone_id, 'zone_name' => (string) ($zone['zone_name'] ?? __('Zone', 'ai-eshop-optimizer')), 'method_id' => $mid, 'method_title' => $title, 'method_label' => method_exists($m, 'get_method_title') ? (string) $m->get_method_title() : $mid, 'is_aieo' => $mid === 'aieo_table_rate', 'is_table_rate'=> $is_table_rate, 'is_enabled' => $is_enabled, 'count' => ($is_table_rate && class_exists('AIEO_TRS_Rules')) ? (int) AIEO_TRS_Rules::count_for_instance($iid) : 0, ); } } return $out; } private function find_instance($instance_id) { $instance_id = (int) $instance_id; foreach ($this->discover_instances() as $row) { if ((int) $row['instance_id'] === $instance_id) { return $row; } } return null; } private function render_rules() { $nonce = wp_create_nonce('aieo_trs_admin'); $instance_id = isset($_GET['instance_id']) ? (int) $_GET['instance_id'] : 0; $instances = $this->discover_instances(); $base_url = admin_url('admin.php?page=' . AIEO_DMM::MENU_SLUG . '&dmm_tab=shipping_rates&trs_section=rules'); $by_zone = array(); foreach ($instances as $row) { $by_zone[$row['zone_id']]['name'] = $row['zone_name']; $by_zone[$row['zone_id']]['rows'][] = $row; } list($aieo_trs_open, $aieo_trs_close) = AIEO_DMM_UI::card(array( 'anchor' => 'shipping-rates-carriers', 'icon' => 'truck', 'title' => __('Carriers', 'ai-eshop-optimizer'), 'description' => __('Replaces the SkyVerge `woocommerce-table-rate-shipping` plugin with an in-memory matcher (object-cached, ~0 DB queries per cart re-render).', 'ai-eshop-optimizer'), )); echo $aieo_trs_open; if (empty($by_zone)) { $aieo_trs_wc = '' . esc_html__('Open WC → Shipping zones', 'ai-eshop-optimizer') . ''; $aieo_trs_wc = \Rooster\Admin\States::state('not-set-up', array( 'title' => __('No table-rate shipping methods found in any zone yet. Add a "Rooster One Table Rate" method to a WooCommerce shipping zone, then return here to configure rules.', 'ai-eshop-optimizer'), 'action' => $aieo_trs_wc, )); echo $aieo_trs_wc; } else { foreach ($by_zone as $zid => $zone) { $zone_edit_url = admin_url('admin.php?page=wc-settings&tab=shipping&zone_id=' . (int) $zid); echo '
'; echo '
'; echo '' . esc_html($zone['name']) . ''; echo \Rooster\Admin\Ui::button(__('Edit zone regions / postcodes', 'ai-eshop-optimizer'), array('variant' => 'ghost', 'size' => 'sm', 'href' => $zone_edit_url, 'external' => true)); echo '
'; foreach ($zone['rows'] as $row) { $is_active = $row['instance_id'] === $instance_id; echo '
'; echo '' . esc_html($row['method_title']); if (!$row['is_enabled']) { $aieo_trs_off = ' ' . \Rooster\Admin\States::status('off') . \Rooster\Admin\Guidance::tip(__('Disabled in WooCommerce shipping zone settings — flipping the calculator is still allowed.', 'ai-eshop-optimizer'), __('off', 'ai-eshop-optimizer')); echo $aieo_trs_off; } echo ''; if ($row['is_table_rate']) { $badge_text = $row['is_aieo'] ? __('AIEO', 'ai-eshop-optimizer') : __('Legacy', 'ai-eshop-optimizer'); echo \Rooster\Admin\Ui::badge($badge_text, $row['is_aieo'] ? 'success' : 'warning', array('sm' => true)); echo '' . sprintf( esc_html(_n('%d rule', '%d rules', (int) $row['count'], 'ai-eshop-optimizer')), (int) $row['count'] ) . ''; echo ''; if ($row['is_aieo']) { $flip_label = __('Switch to legacy', 'ai-eshop-optimizer'); $flip_to = 'table_rate'; $flip_variant = 'ghost'; } else { $flip_label = __('Activate AIEO', 'ai-eshop-optimizer'); $flip_to = 'aieo_table_rate'; $flip_variant = 'primary'; } echo \Rooster\Admin\Ui::button($flip_label, array('variant' => $flip_variant, 'class' => 'aieo-trs-flip', 'attrs' => array('data-instance' => (int) $row['instance_id'], 'data-to' => $flip_to, 'data-nonce' => $nonce))); } else { echo \Rooster\Admin\Ui::badge((string) $row['method_label'], 'mono', array('sm' => true)); echo '' . esc_html__('configured in WC', 'ai-eshop-optimizer') . ''; $wc_method_url = admin_url('admin.php?page=wc-settings&tab=shipping&zone_id=' . (int) $zid . '&instance_id=' . (int) $row['instance_id']); echo '' . esc_html__('Open in WC', 'ai-eshop-optimizer') . ''; } echo '
'; } $add_method_url = admin_url('admin.php?page=wc-settings&tab=shipping&zone_id=' . (int) $zid); echo '
' . \Rooster\Admin\Ui::button(__('Add another carrier or free-shipping rule to this zone', 'ai-eshop-optimizer'), array('variant' => 'ghost', 'size' => 'sm', 'icon' => 'plus', 'href' => $add_method_url, 'external' => true)) . '
'; echo '
'; } $ajax = admin_url('admin-ajax.php'); ?>

' . '' . '

' . AIEO_Kit::button(__('Add rule', 'ai-eshop-optimizer'), array('variant' => 'primary', 'icon' => 'plus', 'attrs' => array('id' => 'trs-add'))) . ' ' . \Rooster\Admin\States::outcome('', '', array('id' => 'trs-status')) . '

' . '
' . AIEO_TRS_Zone_Admin::rules_table(array(), AIEO_TRS_Zone_Admin::shipping_classes()) . '
'; echo \Rooster\Admin\Ui::dialog('aieo-trs-modal', __('Rules', 'ai-eshop-optimizer'), $aieo_trs_body, array( 'confirm' => '', 'cancel' => __('Close', 'ai-eshop-optimizer'), 'wide' => true, 'dialog_attrs' => array('data-nonce' => $nonce), )); echo $this->rule_editor_dialog(); $this->print_grid_js(0, $nonce); $this->print_modal_js($nonce); } private function rule_editor_dialog() { if (!class_exists('\Rooster\Admin\Ui')) { return ''; } $classes = array(); foreach (AIEO_TRS_Zone_Admin::shipping_classes() as $c) { $classes[(string) $c['slug']] = (string) $c['name']; } $weight_unit = function_exists('get_option') ? (string) get_option('woocommerce_weight_unit', 'kg') : 'kg'; $number = static function ($name, $label, $step, $help = '') { return AIEO_Kit::row($label, AIEO_Kit::input($name, '', array('id' => 'aieo-trs-ed-' . $name, 'type' => 'number', 'step' => $step, 'short' => true)), $help, array('id' => 'aieo-trs-ed-' . $name)); }; $body = AIEO_Kit::row(_x('Class', 'a shipping class', 'ai-eshop-optimizer'), AIEO_Kit::select('rate_class', $classes, '', array('id' => 'aieo-trs-ed-rate_class')), __('Any class matches every cart; a class needs a product of that class in the cart, and the rule then counts those products only.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-ed-rate_class')) . AIEO_Kit::row( _x('Condition', 'what a shipping rule tests', 'ai-eshop-optimizer'), AIEO_Kit::select('rate_condition', array( '' => __('Always matches', 'ai-eshop-optimizer'), 'price' => __('Subtotal', 'ai-eshop-optimizer'), 'weight' => __('Weight', 'ai-eshop-optimizer'), 'items' => __('Items', 'ai-eshop-optimizer'), 'items_in_class' => __('Items in class', 'ai-eshop-optimizer'), ), '', array('id' => 'aieo-trs-ed-rate_condition')), __('What Min and Max are compared with. The subtotal is before tax and after discounts.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-ed-rate_condition') ) . $number('rate_min', __('Min', 'ai-eshop-optimizer'), '0.01', __('The rule matches from this value up, the value included. Empty means no lower limit.', 'ai-eshop-optimizer')) . $number('rate_max', __('Max', 'ai-eshop-optimizer'), '0.01', __('The rule matches up to this value, the value included. Empty means no upper limit.', 'ai-eshop-optimizer')) . $number('rate_cost', __('Cost', 'ai-eshop-optimizer'), '0.0001', __('The fixed part of the charge; the three amounts below are added to it.', 'ai-eshop-optimizer')) . $number('rate_cost_per_item', __('+ per item', 'ai-eshop-optimizer'), '0.0001') . $number('rate_cost_per_weight_unit', sprintf(__('+ per %s', 'ai-eshop-optimizer'), $weight_unit), '0.0001') . $number('rate_cost_percent', __('+ % of subtotal', 'ai-eshop-optimizer'), '0.0001') . AIEO_Kit::row(__('Label', 'ai-eshop-optimizer'), AIEO_Kit::input('rate_label', '', array('id' => 'aieo-trs-ed-rate_label', 'medium' => true, 'placeholder' => __('(uses method title)', 'ai-eshop-optimizer'))), __('The name the shopper sees for this rate at checkout.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-ed-rate_label')) . AIEO_Kit::row(__('Stop after match', 'ai-eshop-optimizer'), AIEO_Kit::check('rate_priority', __('Yes', 'ai-eshop-optimizer'), false, array('id' => 'aieo-trs-ed-rate_priority')), __('When this rule matches, it replaces the rules of its class matched before it, and the ones after it are skipped.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-ed-rate_priority', 'group' => true)) . AIEO_Kit::row(__('Abort method', 'ai-eshop-optimizer'), AIEO_Kit::check('rate_abort', __('Yes', 'ai-eshop-optimizer'), false, array('id' => 'aieo-trs-ed-rate_abort')), __('When this rule matches, the shipping method is not offered for this cart.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-ed-rate_abort', 'group' => true)) . AIEO_Kit::row(__('Reason shown to customer', 'ai-eshop-optimizer'), AIEO_Kit::input('rate_abort_reason', '', array('id' => 'aieo-trs-ed-rate_abort_reason')), __('Shown as a notice at checkout when Abort method takes the shipping method away.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-ed-rate_abort_reason')) . AIEO_Kit::row(__('Status', 'ai-eshop-optimizer'), AIEO_Kit::check('enabled', __('Enabled', 'ai-eshop-optimizer'), true, array('id' => 'aieo-trs-ed-enabled')), __('A disabled rule is kept but never matches.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-ed-enabled', 'group' => true)); $footer = AIEO_Kit::button(__('Cancel', 'ai-eshop-optimizer'), array('attrs' => array('data-ra-dialog-close' => ''))) . ' ' . AIEO_Kit::button(__('Save', 'ai-eshop-optimizer'), array('variant' => 'primary', 'type' => 'submit', 'attrs' => array('id' => 'trs-save'))) . ' ' . \Rooster\Admin\States::outcome('', 'destructive', array('id' => 'trs-editor-msg')); return \Rooster\Admin\Ui::dialog('aieo-trs-editor', __('Add rule', 'ai-eshop-optimizer'), $body, array('footer' => $footer, 'form' => array('method' => 'dialog'), 'wide' => true)); } private function print_modal_js($nonce) { ?> __('Add rule', 'ai-eshop-optimizer'), 'save' => __('Save', 'ai-eshop-optimizer'), 'cancel' => __('Cancel', 'ai-eshop-optimizer'), 'delete' => __('Delete', 'ai-eshop-optimizer'), 'edit' => __('Edit', 'ai-eshop-optimizer'), 'duplicate' => __('Duplicate', 'ai-eshop-optimizer'), 'class' => _x('Class', 'a shipping class', 'ai-eshop-optimizer'), 'class_any' => __('Any class', 'ai-eshop-optimizer'), 'class_none' => __('No class', 'ai-eshop-optimizer'), 'condition' => _x('Condition', 'what a shipping rule tests', 'ai-eshop-optimizer'), 'min' => __('Min', 'ai-eshop-optimizer'), 'max' => __('Max', 'ai-eshop-optimizer'), 'cost' => __('Cost', 'ai-eshop-optimizer'), 'per_item' => __('+ per item', 'ai-eshop-optimizer'), 'per_weight' => sprintf(__('+ per %s', 'ai-eshop-optimizer'), $weight_unit), 'percent' => __('+ % of subtotal', 'ai-eshop-optimizer'), 'label' => __('Label', 'ai-eshop-optimizer'), 'no_label' => __('(uses method title)', 'ai-eshop-optimizer'), 'priority' => __('Stop after match', 'ai-eshop-optimizer'), 'abort' => __('Abort method', 'ai-eshop-optimizer'), 'abort_reason' => __('Reason shown to customer', 'ai-eshop-optimizer'), 'enabled' => __('Enabled', 'ai-eshop-optimizer'), 'disabled' => __('Disabled', 'ai-eshop-optimizer'), 'confirm_delete' => __('Delete this rule?', 'ai-eshop-optimizer'), 'cond_price' => __('Subtotal', 'ai-eshop-optimizer'), 'cond_weight'=> __('Weight', 'ai-eshop-optimizer'), 'cond_items' => __('Items', 'ai-eshop-optimizer'), 'cond_items_in_class' => __('Items in class', 'ai-eshop-optimizer'), 'cond_none' => __('Always matches', 'ai-eshop-optimizer'), 'currency' => $currency_symbol, 'currency_pos' => function_exists('get_option') ? (string) get_option('woocommerce_currency_pos', 'left') : 'left', 'weight_unit'=> $weight_unit, ); ?> discover_instances(); $ajax = admin_url('admin-ajax.php'); if (function_exists('rooster_admin_band_open')) { rooster_admin_band_open(array('cols' => 2)); } list($aieo_trs_open, $aieo_trs_close) = AIEO_DMM_UI::card(array( 'anchor' => 'shipping-rates-export', 'icon' => 'download', 'title' => __('Export', 'ai-eshop-optimizer'), 'description' => __('Download all current rules as a CSV. Pick a single zone-method to limit the export, or export everything.', 'ai-eshop-optimizer'), 'col' => 1, 'footer' => '', )); echo $aieo_trs_open; $aieo_trs_export = array('' => __('All zone-methods', 'ai-eshop-optimizer')); foreach ($instances as $row) { $aieo_trs_export[(string) (int) $row['instance_id']] = $row['zone_name'] . ' — #' . $row['instance_id']; } echo '
'; echo ''; echo ''; echo AIEO_Kit::row(__('Shipping method', 'ai-eshop-optimizer'), AIEO_Kit::select('instance_id', $aieo_trs_export, '', array('id' => 'aieo-trs-csv-export-instance')), '', array('id' => 'aieo-trs-csv-export-instance')); echo '
'; echo $aieo_trs_close; list($aieo_trs_open, $aieo_trs_close) = AIEO_DMM_UI::card(array( 'anchor' => 'shipping-rates-import', 'icon' => 'import', 'title' => __('Import', 'ai-eshop-optimizer'), 'description' => __('Upload a CSV with the legacy plugin\'s column shape. Re-uploading the same file is safe — rows with the same (method_id, class, condition, min, max) tuple update in place.', 'ai-eshop-optimizer'), 'col' => 2, 'footer' => '' . ' ' . \Rooster\Admin\States::outcome('', '', array('id' => 'aieo-trs-csv-status')), )); echo $aieo_trs_open; echo '
'; echo ''; $aieo_trs_import = array('' => __('Use shipping_method_id from CSV', 'ai-eshop-optimizer')); foreach ($instances as $row) { $aieo_trs_import[(string) (int) $row['instance_id']] = sprintf( __('%s (for rows without a method)', 'ai-eshop-optimizer'), $row['zone_name'] . ' — #' . $row['instance_id'] ); } echo AIEO_Kit::row(__('CSV file', 'ai-eshop-optimizer'), '', __('The columns of an export from the card beside this one: download one first to see them.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-csv-file', 'required' => true)); echo AIEO_Kit::row(__('Shipping method', 'ai-eshop-optimizer'), AIEO_Kit::select('instance_id', $aieo_trs_import, '', array('id' => 'aieo-trs-csv-import-instance')), __('Rows that name their own shipping method keep it; the method chosen here goes to the rows that leave it blank.', 'ai-eshop-optimizer'), array('id' => 'aieo-trs-csv-import-instance')); echo '
'; ?> 'shipping-rates-legacy-data-audit-2', 'icon' => 'search', 'title' => __('Legacy data audit', 'ai-eshop-optimizer'), 'footer' => $aieo_trs_run ? '' . ' ' . \Rooster\Admin\States::outcome('', '', array('id' => 'aieo-trs-migrate-status')) : '', )); echo $aieo_trs_open; if (!$aieo_trs_ready) { echo \Rooster\Admin\States::state('not-set-up', array('title' => __('Migrator unavailable.', 'ai-eshop-optimizer'))); echo $aieo_trs_close; return; } $nonce = wp_create_nonce('aieo_trs_admin'); if (!$aieo_trs_run) { echo \Rooster\Admin\States::state('all-done', array('title' => __('No legacy `wp_woocommerce_shipping_table_rates` table found. Nothing to migrate.', 'ai-eshop-optimizer'))); echo $aieo_trs_close; return; } echo '

'; echo sprintf( esc_html__('Legacy table has %1$d rules. %2$d already migrated.', 'ai-eshop-optimizer'), (int) $audit['legacy_rows'], (int) $audit['already_migrated'] ); echo '

'; $orphans = isset($audit['orphan_zone_methods']) ? (int) $audit['orphan_zone_methods'] : 0; if ($orphans > 0) { $aieo_trs_warn = sprintf( esc_html__('%d WooCommerce zone-method row(s) still reference the legacy `table_rate` slug and emit no rates. Re-run the migration below to flip them onto `aieo_table_rate`.', 'ai-eshop-optimizer'), $orphans ); $aieo_trs_warn = class_exists('\Rooster\Admin\Ui') ? \Rooster\Admin\Ui::notice($aieo_trs_warn, 'warning', array('inline' => true)) : '

' . $aieo_trs_warn . '

'; echo $aieo_trs_warn; } if (!empty($audit['by_instance'])) { $aieo_trs_rows = array(); foreach ($audit['by_instance'] as $r) { $aieo_trs_rows[] = array( 'instance' => '#' . (int) $r['shipping_method_id'], 'instance_n' => (int) $r['shipping_method_id'], 'rules' => (int) $r['rows_count'], ); } AIEO_Table::render(array( 'id' => 'aieo-trs-legacy-audit', 'page_id' => 'shipping_rates', 'sortable' => true, 'narrow' => true, 'rows' => $aieo_trs_rows, 'columns' => array( array('key' => 'instance', 'label' => __('Method instance', 'ai-eshop-optimizer'), 'type' => 'id', 'align' => 'left', 'sort_key' => 'instance_n'), array('key' => 'rules', 'label' => __('Rule rows', 'ai-eshop-optimizer'), 'type' => 'integer'), ), )); } echo $aieo_trs_close; $ajax = admin_url('admin-ajax.php'); ?>