array('label' => __('Commercial', 'ai-eshop-optimizer'), 'icon' => 'banknote'), 'related' => array('label' => __('Related', 'ai-eshop-optimizer'), 'icon' => 'link'), ); } public function get_tab_base_url() { $tax = AIEO_DMM_Supplier_Extras_Data::managed_taxonomies(); $tax = !empty($tax) ? (string) reset($tax) : 'supplier'; return admin_url('edit-tags.php?taxonomy=' . rawurlencode($tax)); } public function register_hooks() { add_action('init', array($this, 'register_supplier_taxonomy'), 9); $migrator_file = AIEO_DIR . 'includes/dmm/migrators/class-aieo-dmm-supplier-pods-migrator.php'; if (file_exists($migrator_file)) { require_once $migrator_file; if (class_exists('AIEO_DMM_Supplier_Pods_Migrator')) { AIEO_DMM_Supplier_Pods_Migrator::register(); } } add_filter('aieo_dmm_related_tab_taxonomies', array($this, 'register_supplier_in_related_tab')); add_filter('aieo_dmm_related_tab_axes', array($this, 'register_supplier_axis')); add_filter('aieo_dmm_related_tab_axes_for_source', array($this, 'limit_supplier_axes'), 10, 2); if (class_exists('AIEO_DMM_Related_Tab')) { add_action('edited_supplier', array('AIEO_DMM_Related_Tab', 'on_term_save'), 10, 2); add_action('created_supplier', array('AIEO_DMM_Related_Tab', 'on_term_save'), 10, 2); } if (!is_admin()) { return; } add_action('admin_enqueue_scripts', array($this, 'enqueue_term_admin_assets')); foreach (AIEO_DMM_Supplier_Extras_Data::managed_taxonomies() as $tax) { add_action($tax . '_edit_form_fields', array($this, 'render_term_extras'), 1, 2); add_action('edited_' . $tax, array($this, 'save_term_extras'), 10, 2); add_action('created_' . $tax, array($this, 'save_term_extras'), 10, 2); } } public function enqueue_term_admin_assets($hook) { if ($hook !== 'term.php') { return; } $tax = isset($_GET['taxonomy']) ? sanitize_key(wp_unslash($_GET['taxonomy'])) : ''; if (!in_array($tax, AIEO_DMM_Supplier_Extras_Data::managed_taxonomies(), true)) { return; } wp_enqueue_media(); wp_enqueue_editor(); wp_enqueue_style( 'aieo-dmm-category-extras-admin', AIEO_URI . 'assets/css/aieo-dmm-category-extras-admin.css', array(), AIEO_VERSION ); wp_enqueue_script( 'aieo-dmm-category-extras-admin', AIEO_URI . 'assets/js/aieo-dmm-category-extras-admin.js', array('jquery'), AIEO_VERSION, true ); wp_localize_script('aieo-dmm-category-extras-admin', 'aieoCategoryExtras', array( 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce(AIEO_DMM::NONCE_ACTION), 'i18n' => array( 'noResults' => __('No matches.', 'ai-eshop-optimizer'), 'searching' => __('Searching…', 'ai-eshop-optimizer'), 'pickImage' => __('Select image', 'ai-eshop-optimizer'), 'useImage' => __('Use this image', 'ai-eshop-optimizer'), ), )); } public function register_supplier_in_related_tab(array $taxonomies) { if (!in_array('supplier', $taxonomies, true)) { $taxonomies[] = 'supplier'; } return $taxonomies; } public function register_supplier_axis(array $axes) { if (!isset($axes['supplier'])) { $axes['supplier'] = array('term', 'supplier', __('Related suppliers', 'ai-eshop-optimizer'), 'term:supplier'); } return $axes; } public function limit_supplier_axes($axes, $source) { if ($source === 'supplier') { return array_intersect_key((array) $axes, array('product_brand' => true)); } return $axes; } public function register_supplier_taxonomy() { if (taxonomy_exists('supplier')) { return; } register_taxonomy('supplier', array('product'), array( 'labels' => array( 'name' => __('Suppliers', 'ai-eshop-optimizer'), 'singular_name' => __('Supplier', 'ai-eshop-optimizer'), 'menu_name' => __('Suppliers', 'ai-eshop-optimizer'), 'all_items' => __('All Suppliers', 'ai-eshop-optimizer'), 'search_items' => __('Search Suppliers', 'ai-eshop-optimizer'), 'edit_item' => __('Edit Supplier', 'ai-eshop-optimizer'), 'update_item' => __('Update Supplier', 'ai-eshop-optimizer'), 'add_new_item' => __('Add New Supplier', 'ai-eshop-optimizer'), 'new_item_name' => __('New Supplier Name', 'ai-eshop-optimizer'), 'not_found' => __('No suppliers found.', 'ai-eshop-optimizer'), ), 'public' => false, 'publicly_queryable' => false, 'show_ui' => true, 'show_in_menu' => true, 'show_admin_column' => false, 'show_in_rest' => true, 'hierarchical' => false, 'query_var' => false, 'rewrite' => false, 'capabilities' => array( 'manage_terms' => 'manage_woocommerce', 'edit_terms' => 'manage_woocommerce', 'delete_terms' => 'manage_woocommerce', 'assign_terms' => 'edit_products', ), )); } public function render_term_extras($term, $taxonomy) { if (!is_object($term) || !isset($term->term_id)) { return; } if (!in_array($taxonomy, AIEO_DMM_Supplier_Extras_Data::managed_taxonomies(), true)) { return; } $row = AIEO_DMM_Supplier_Extras_Data::get($taxonomy, (int) $term->term_id); ?>
term_id, 'aieo_se_nonce'); ?>

'aieo_se[supplier_owner_user_id]', 'id' => 'aieo-se-owner', 'selected' => (int) ($row['supplier_owner_user_id'] ?? 0), 'show_option_none' => __('— none —', 'ai-eshop-optimizer'), 'option_none_value' => 0, 'capability' => 'manage_woocommerce', )); ?>

'aieo_se[notes]', 'textarea_rows' => 5, 'media_buttons' => false, 'teeny' => true, 'quicktags' => true, ) ); ?>

prefix . 'aieo_dmm_supplier_sku_map'; if (function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($aieo_se_map)) { $aieo_se_n = (int) $GLOBALS['wpdb']->get_var($GLOBALS['wpdb']->prepare( "SELECT COUNT(*) FROM `{$aieo_se_map}` WHERE supplier_term_id = %d", (int) $term->term_id )); echo '

' . esc_html(sprintf( _n('%d product SKU is mapped to this supplier from the ERP catalogue.', '%d product SKUs are mapped to this supplier from the ERP catalogue.', $aieo_se_n, 'ai-eshop-optimizer'), $aieo_se_n )) . '

'; } if (class_exists('AIEO_DMM_Related_Tab')) { AIEO_DMM_Related_Tab::render($taxonomy, (int) $term->term_id); } else { echo '

' . esc_html__('Related-tab component unavailable.', 'ai-eshop-optimizer') . '

'; } ?>
taxonomy, AIEO_DMM_Supplier_Extras_Data::managed_taxonomies(), true)) { return; } $raw = wp_unslash($_POST['aieo_se']); if (!is_array($raw)) { return; } $patch = array(); foreach (array( 'email', 'lead_time_days', 'payment_terms_days', 'special_discount_pct', 'supplier_owner_user_id', 'notes', 'erp_id', 'erp_sku_code', 'vat_code', 'address_line_1', 'address_region', 'address_city', 'address_postal_code', 'tel_1', 'tel_2', 'fax', 'corporate_url', 'corporate_email', ) as $field) { if (isset($raw[$field])) { $patch[$field] = $raw[$field]; } } AIEO_DMM_Supplier_Extras_Data::save($term->taxonomy, $term_id, $patch); } public function render_subtab() { global $wpdb; $supplier_count = (int) $wpdb->get_var('SELECT COUNT(*) FROM ' . AIEO_DMM_Supplier_Extras_Data::table()); $directory = $this->supplier_directory(); $has_pods = class_exists('AIEO_DMM_Supplier_Pods_Migrator') && AIEO_DMM_Supplier_Pods_Migrator::has_pods_source(); list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'supplier-extras-overview', 'icon' => 'circle-question-mark', 'title' => __('Supplier metadata', 'ai-eshop-optimizer'), 'description' => __('Pods-free per-supplier metadata. Edit individual suppliers at WooCommerce → Products → Suppliers → click a supplier. Related Brands are stored bidirectionally in wp_aieo_dmm_term_relations.', 'ai-eshop-optimizer'), 'col' => 2, )); echo $open; echo '
'; echo \Rooster\Admin\Ui::stat(__('Supplier metadata rows', 'ai-eshop-optimizer'), number_format_i18n($supplier_count)); echo '
'; if (empty($directory)) { $empty = \Rooster\Admin\States::state( 'first-use', array( 'title' => __('No suppliers yet. Create them at Products → Suppliers, or let a feed source create one.', 'ai-eshop-optimizer'), 'compact' => true, ) ); echo $empty; } if (!$has_pods) { echo \Rooster\Admin\Guidance::text(__('Pods is not active here. Edit suppliers directly on each supplier term-edit screen.', 'ai-eshop-optimizer')); } echo $close; if (!empty($directory)) { $this->render_supplier_directory($directory); } if ($has_pods) { $audit = AIEO_DMM_Supplier_Pods_Migrator::audit(); $nonce = wp_create_nonce('aieo_supplier_pods_migrate'); $ajax = admin_url('admin-ajax.php'); list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'supplier-extras-pods-supplier-extras-migrator', 'icon' => 'import', 'title' => __('Migrate suppliers from Pods', 'ai-eshop-optimizer'), 'description' => __('Copies supplier fields from the Pods pod into Supplier Extras in batches, and shows how many supplier terms are already migrated.', 'ai-eshop-optimizer'), 'col' => 2, )); echo $open; echo '

'; echo sprintf( esc_html__('Pods is active on `%1$s`. %2$d supplier terms total, %3$d already migrated.', 'ai-eshop-optimizer'), esc_html($audit['taxonomy']), (int) $audit['supplier_terms'], (int) $audit['already_migrated'] ); echo '

'; $job = \Rooster\Admin\States::job(array( 'id' => 'aieo-supplier-pods-job', 'state' => 'idle', 'run' => __('Run migration', 'ai-eshop-optimizer'), 'run_attrs' => array('id' => 'aieo-supplier-pods-run'), )); echo $job; ?> render_legacy_tables_card(); } private function supplier_directory() { global $wpdb; $tax = AIEO_DMM_Supplier_Extras_Data::managed_taxonomies(); $tax = !empty($tax) ? (string) reset($tax) : 'supplier'; $limit = 500; $terms = taxonomy_exists($tax) ? get_terms(array('taxonomy' => $tax, 'hide_empty' => false, 'number' => $limit, 'orderby' => 'name')) : array(); if (is_wp_error($terms) || empty($terms)) { return null; } $total = (int) wp_count_terms(array('taxonomy' => $tax, 'hide_empty' => false)); $extras = array(); foreach ((array) $wpdb->get_results($wpdb->prepare( 'SELECT * FROM ' . AIEO_DMM_Supplier_Extras_Data::table() . ' WHERE taxonomy = %s', $tax ), ARRAY_A) as $r) { $extras[(int) $r['term_id']] = $r; } $brand_counts = array(); $be = $wpdb->prefix . 'aieo_dmm_brand_extras'; if (aieo_dmm_table_exists($be)) { foreach ((array) $wpdb->get_results("SELECT supplier_term_id, COUNT(*) n FROM `{$be}` WHERE supplier_term_id IS NOT NULL GROUP BY supplier_term_id", ARRAY_A) as $r) { $brand_counts[(int) $r['supplier_term_id']] = (int) $r['n']; } } $sku_counts = array(); $map = $wpdb->prefix . 'aieo_dmm_supplier_sku_map'; if (aieo_dmm_table_exists($map)) { foreach ((array) $wpdb->get_results("SELECT supplier_term_id, COUNT(*) n FROM `{$map}` WHERE supplier_term_id IS NOT NULL GROUP BY supplier_term_id", ARRAY_A) as $r) { $sku_counts[(int) $r['supplier_term_id']] = (int) $r['n']; } } $rows = array(); foreach ($terms as $term) { $tid = (int) $term->term_id; $row = $extras[$tid] ?? array(); $rows[] = array( 'tid' => $tid, 'name' => (string) $term->name, 'edit' => (string) get_edit_term_link($tid, $tax), 'erp_id' => (string) ($row['erp_id'] ?? ''), 'email' => (string) ($row['email'] ?? ''), 'lead' => (isset($row['lead_time_days']) && '' !== $row['lead_time_days'] && null !== $row['lead_time_days']) ? (int) $row['lead_time_days'] : null, 'payment' => (isset($row['payment_terms_days']) && '' !== $row['payment_terms_days'] && null !== $row['payment_terms_days']) ? (int) $row['payment_terms_days'] : null, 'brands' => $brand_counts[$tid] ?? null, 'skus' => $sku_counts[$tid] ?? null, 'products' => (int) $term->count > 0 ? (int) $term->count : null, ); } return array('rows' => $rows, 'limit' => (int) $limit, 'total' => (int) $total); } private function render_supplier_directory(array $directory) { $limit = (int) $directory['limit']; $total = (int) $directory['total']; list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'supplier-extras-directory', 'icon' => 'table', 'title' => __('Supplier directory', 'ai-eshop-optimizer'), 'description' => __('Every supplier and how much of the catalogue it reaches. Click a name to edit it — the fields live on the supplier term screen at Products → Suppliers.', 'ai-eshop-optimizer'), 'footnote' => $total > $limit ? __('The full list is at Products → Suppliers.', 'ai-eshop-optimizer') : '', 'flush' => true, 'span' => 'all', )); echo $open; echo '

' . esc_html__('The Suppliers page edits every field of every supplier in one place.', 'ai-eshop-optimizer') . '

'; AIEO_Table::card(array( 'wrap' => false, 'page_id' => $this->get_id(), 'row_key' => 'tid', 'rows' => $directory['rows'], 'cap' => $limit, 'total' => $total, 'capped' => $total > $limit, 'columns' => array( array('key' => 'name', 'label' => __('Supplier', 'ai-eshop-optimizer'), 'type' => 'link', 'format' => array('href' => 'edit')), array('key' => 'erp_id', 'label' => __('ERP id', 'ai-eshop-optimizer'), 'type' => 'code'), array('key' => 'email', 'label' => __('Email', 'ai-eshop-optimizer')), array('key' => 'lead', 'label' => __('Lead time (days)', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'payment', 'label' => __('Payment terms (days)', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'brands', 'label' => __('Brands', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'skus', 'label' => __('Mapped SKUs', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'products', 'label' => __('Products (direct)', 'ai-eshop-optimizer'), 'type' => 'integer'), ), )); echo $close; } private function render_legacy_tables_card() { if (!class_exists('AIEO_DMM_Supplier_Pods_Migrator') || !AIEO_DMM_Supplier_Pods_Migrator::has_legacy_tables()) { return; } $audit = AIEO_DMM_Supplier_Pods_Migrator::audit(); $nonce = wp_create_nonce('aieo_supplier_pods_migrate'); $ajax = admin_url('admin-ajax.php'); $legacy = (array) ($audit['legacy'] ?? array()); $label_map = array( 'supplier_fields' => __('Supplier fields (Pods)', 'ai-eshop-optimizer'), 'supplier_sku_fields' => __('Supplier fields with ERP link (Pods)', 'ai-eshop-optimizer'), 'erp_master' => __('ERP supplier master', 'ai-eshop-optimizer'), 'store_sales' => __('Store sales rows carrying a supplier name', 'ai-eshop-optimizer'), 'preorders' => __('Preorder history', 'ai-eshop-optimizer'), ); list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'supplier-extras-legacy-tables', 'icon' => 'database', 'title' => __('Migrate the legacy supplier tables', 'ai-eshop-optimizer'), 'description' => __('The old implementation\'s raw tables still exist in the database, even though Pods itself is gone. This reads them directly and lands everything in the new schema: supplier fields and the ERP identity into Supplier metadata (only filling blanks, never overwriting your edits), per-SKU supplier names into the SKU map, and the preorder history into the preorder log. Safe to run again — nothing is copied twice.', 'ai-eshop-optimizer'), 'col' => 2, )); echo $open; $legacy_rows = array(); foreach ($label_map as $key => $label) { $n = $legacy[$key] ?? null; $legacy_rows[] = array( '_id' => (string) $key, 'label' => $label, 'value' => $n === null ? array('html' => '' . esc_html__('table absent', 'ai-eshop-optimizer') . '') : number_format_i18n((int) $n), ); } $legacy_rows[] = array( '_id' => 'already_in_new_schema', 'label' => __('Already in the new schema', 'ai-eshop-optimizer'), 'value' => sprintf( __('%1$s suppliers with an ERP identity · %2$s SKU-map rows · %3$s preorder rows', 'ai-eshop-optimizer'), number_format_i18n((int) ($audit['extras_with_erp_identity'] ?? 0)), number_format_i18n((int) ($audit['sku_map_rows'] ?? 0)), number_format_i18n((int) ($audit['legacy_preorders_migrated'] ?? 0)) ), ); echo \Rooster\Admin\Ui::table( array( array('key' => 'label', 'label' => __('Legacy table', 'ai-eshop-optimizer'), 'type' => 'text'), array('key' => 'value', 'label' => __('Rows', 'ai-eshop-optimizer'), 'type' => 'text'), ), $legacy_rows, array('id' => 'aieo-supplier-legacy-counts', 'caption' => __('Legacy supplier tables', 'ai-eshop-optimizer')) ); $job = \Rooster\Admin\States::job(array( 'id' => 'aieo-supplier-legacy-job', 'state' => 'idle', 'run' => __('Migrate legacy tables', 'ai-eshop-optimizer'), 'run_attrs' => array('id' => 'aieo-supplier-legacy-run'), )); echo $job; ?>