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');
?>
'
. '' . \Rooster\Admin\Ui::notice(esc_html($aieo_trs_legacy), 'warning', array('inline' => true)) . '
'
. ''
. 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 $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 '';
?>
'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))
: '';
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');
?>