<?php

if (!defined('ABSPATH')) { exit; }

class AIEO_DMM_Fulfillment extends AIEO_DMM_Module_Base {

    public function get_id()       { return 'fulfillment'; }
    public function get_label()    { return __('Order Documents & Couriers', 'ai-eshop-optimizer'); }
    public function get_priority() { return 14; }
    public function get_group()    { return 'import_export'; }

    public function is_pro_locked() { return true; }
    public function get_pro_promo() {
        return array(
            'title'       => __('Order Documents & Courier Fulfillment', 'ai-eshop-optimizer'),
            'description' => __('Order-to-shipment workflow with carrier label generation, batch printing, and per-zone fulfillment SLAs — turns a stack of unprinted orders into a tray of ready-to-ship parcels in a single click.', 'ai-eshop-optimizer'),
            'bullets'     => array(
                __('Bulk label generation for the major Greek + EU couriers.', 'ai-eshop-optimizer'),
                __('Configurable invoice + delivery-note templates.', 'ai-eshop-optimizer'),
                __('Per-zone SLA tracking so you spot the slow lanes before customers do.', 'ai-eshop-optimizer'),
            ),
        );
    }

    public function __construct() {
        $this->load_services();
    }

    private function load_services() {
        $base = AIEO_DIR . 'includes/dmm/fulfillment/';

        
        foreach (array(
            'class-aieo-courier-driver-base.php',
            'class-aieo-order-document-base.php',
            
            
            
            'class-aieo-runtime-fulfillment-urls.php',
            
            
            
            
            'class-aieo-runtime-fulfillment-tracking.php',
            'class-aieo-fulfillment-settings.php',
            'class-aieo-fulfillment-vouchers.php',
            'class-aieo-fulfillment-pdf-engine.php',
            'class-aieo-fulfillment-templates.php',
            'class-aieo-fulfillment-documents.php',
            'class-aieo-fulfillment-cron.php',
            'class-aieo-fulfillment-closings.php',
            'class-aieo-fulfillment-emails.php',
            'class-aieo-fulfillment-bulk.php',
            'class-aieo-fulfillment-migrator.php',
        ) as $f) {
            $path = $base . $f;
            if (file_exists($path)) { require_once $path; }
        }
        if (class_exists('AIEO_Fulfillment_Cron'))                { AIEO_Fulfillment_Cron::register(); }
        if (class_exists('AIEO_Fulfillment_Emails'))              { AIEO_Fulfillment_Emails::register(); }
        if (class_exists('AIEO_Runtime_Fulfillment_Tracking'))    { AIEO_Runtime_Fulfillment_Tracking::register(); }

        
        
        $drivers_dir = $base . 'drivers/';
        if (is_dir($drivers_dir)) {
            foreach (glob($drivers_dir . 'class-aieo-courier-driver-*.php') as $driver_path) {
                require_once $driver_path;
            }
        }
        $docs_dir = $base . 'docs/';
        if (is_dir($docs_dir)) {
            foreach (glob($docs_dir . 'class-aieo-order-document-*.php') as $doc_path) {
                require_once $doc_path;
            }
        }
    }

    public function register_hooks() {
        
        add_action('add_meta_boxes', array($this, 'register_meta_box'), 30, 2);

        
        add_action('wp_ajax_aieo_ff_save_courier',    array($this, 'ajax_save_courier'));
        add_action('wp_ajax_aieo_ff_test_courier',    array($this, 'ajax_test_courier'));
        add_action('wp_ajax_aieo_ff_create_voucher',  array($this, 'ajax_create_voucher'));
        add_action('wp_ajax_aieo_ff_save_manual',     array($this, 'ajax_save_manual'));
        add_action('wp_ajax_aieo_ff_get_label',       array($this, 'ajax_get_label'));
        add_action('wp_ajax_aieo_ff_cancel_voucher',  array($this, 'ajax_cancel_voucher'));
        add_action('wp_ajax_aieo_ff_track_now',       array($this, 'ajax_track_now'));
        add_action('wp_ajax_aieo_ff_backfill_delivered', array($this, 'ajax_backfill_delivered_batch'));
        add_action('wp_ajax_aieo_ff_backfill_costs',     array($this, 'ajax_backfill_costs_batch'));
        add_action('wp_ajax_aieo_ff_import_geniki_csv',  array($this, 'ajax_import_geniki_invoice_csv'));
        add_action('wp_ajax_aieo_ff_import_carrier_csv', array($this, 'ajax_import_carrier_invoice_csv'));
        add_action('wp_ajax_aieo_ff_backfill_customer_paid', array($this, 'ajax_backfill_customer_paid_batch'));
        add_action('wp_ajax_aieo_ff_backfill_detailed', array($this, 'ajax_backfill_detailed_batch'));
        add_action('wp_ajax_aieo_ff_metrics_drill',      array($this, 'ajax_metrics_drill'));
        add_action('wp_ajax_aieo_ff_metrics_export',     array($this, 'ajax_metrics_export'));
        add_action('wp_ajax_aieo_ff_metrics_export_aggregate', array($this, 'ajax_metrics_export_aggregate'));
        add_action('wp_ajax_aieo_ff_save_general',    array($this, 'ajax_save_general_settings'));
        add_action('wp_ajax_aieo_ff_delete_voucher',  array($this, 'ajax_delete_voucher'));

        
        add_action('wp_ajax_aieo_ff_create_document', array($this, 'ajax_create_document'));
        add_action('wp_ajax_aieo_ff_get_document_pdf',array($this, 'ajax_get_document_pdf'));
        add_action('wp_ajax_aieo_ff_print_doc_for_order', array($this, 'ajax_print_doc_for_order'));
        add_action('wp_ajax_aieo_ff_save_manual_voucher', array($this, 'ajax_save_manual_voucher'));
        add_action('wp_ajax_aieo_ff_save_carrier_map',    array($this, 'ajax_save_carrier_map'));
        add_action('wp_ajax_aieo_ff_delete_document', array($this, 'ajax_delete_document'));

        
        add_action('wp_ajax_aieo_ff_bulk_merge',         array($this, 'ajax_bulk_merge'));
        add_action('wp_ajax_aieo_ff_bulk_zip',           array($this, 'ajax_bulk_zip'));
        add_action('wp_ajax_aieo_ff_bulk_zip_labels',    array($this, 'ajax_bulk_zip_labels'));
        add_action('wp_ajax_aieo_ff_close_pending_now',  array($this, 'ajax_close_pending_now'));
        add_action('wp_ajax_aieo_ff_close_pending_by_date', array($this, 'ajax_close_pending_by_date'));
        add_action('wp_ajax_aieo_ff_toggle_auto_close',  array($this, 'ajax_toggle_auto_close'));
        
        
        
        
        add_action('wp_ajax_aieo_ff_close_and_print',    array($this, 'ajax_close_and_print'));
        add_action('wp_ajax_aieo_ff_reprint_last_closing', array($this, 'ajax_reprint_last_closing'));
        add_action('wp_ajax_aieo_ff_download_manifest',  array($this, 'ajax_download_manifest'));

        
        add_action('wp_ajax_aieo_ff_migrate_audit',      array($this, 'ajax_migrate_audit'));
        add_action('wp_ajax_aieo_ff_migrate_batch',      array($this, 'ajax_migrate_batch'));
        add_action('wp_ajax_aieo_ff_migrate_reset',      array($this, 'ajax_migrate_reset'));

        
        
        $hpos_screen   = function_exists('wc_get_page_screen_id') ? wc_get_page_screen_id('shop-order') : 'woocommerce_page_wc-orders';
        $legacy_screen = 'edit-shop_order';
        foreach (array($hpos_screen, $legacy_screen) as $screen) {
            
            add_filter("manage_{$screen}_columns",          array($this, 'orders_list_columns'));
            
            add_action("manage_{$screen}_custom_column",    array($this, 'orders_list_column_body'), 10, 2);
            
            add_filter("bulk_actions-{$screen}",            array($this, 'orders_list_bulk_actions'));
            add_filter("handle_bulk_actions-{$screen}",     array($this, 'orders_list_handle_bulk'), 10, 3);
        }
        
        add_action('admin_notices', array($this, 'orders_list_bulk_notice'));
        
        
        
        add_action('admin_init', array($this, 'maybe_render_a4x3_print_page'));
        
        
        
        
        
        add_action('woocommerce_order_list_table_restrict_manage_orders', array($this, 'render_channel_filter_dropdown'), 20, 2);
        add_filter('woocommerce_order_list_table_prepare_items_query_args', array($this, 'apply_channel_filter_to_query'));
        
        add_action('restrict_manage_posts',                                 array($this, 'render_channel_filter_dropdown_legacy'), 20);
        add_action('pre_get_posts',                                         array($this, 'apply_channel_filter_to_legacy_query'));
        
        add_action('admin_footer', array($this, 'orders_list_doc_button_js'));
        
        
        add_action('admin_footer', array($this, 'orders_list_voucher_modal'));

        
        
        
        
        
        add_filter('aieo_fulfillment_couriers', array($this, 'register_custom_carriers'), 50);

        
        add_action('wp_ajax_aieo_ff_save_custom_carrier',   array($this, 'ajax_save_custom_carrier'));
        add_action('wp_ajax_aieo_ff_delete_custom_carrier', array($this, 'ajax_delete_custom_carrier'));
        add_action('wp_ajax_aieo_ff_merge_couriers',        array($this, 'ajax_merge_couriers'));
    }

    
    public function register_custom_carriers($drivers) {
        if (!class_exists('AIEO_Courier_Driver_Custom')) { return $drivers; }
        if (!is_array($drivers)) { $drivers = array(); }
        foreach ($this->get_custom_carriers() as $slug => $cfg) {
            $code = AIEO_Courier_Driver_Custom::PREFIX . $slug;
            if (!isset($drivers[$code])) {
                $drivers[$code] = new AIEO_Courier_Driver_Custom(
                    $slug,
                    isset($cfg['label']) ? (string) $cfg['label'] : '',
                    isset($cfg['tracking_url_template']) ? (string) $cfg['tracking_url_template'] : ''
                );
            }
        }
        return $drivers;
    }

    
    public function orders_list_doc_button_js() {
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        if (!$screen) { return; }
        $hpos   = function_exists('wc_get_page_screen_id') ? wc_get_page_screen_id('shop-order') : '';
        if (!in_array($screen->id, array($hpos, 'edit-shop_order'), true)) { return; }
        
        
        $nonce   = wp_create_nonce('aieo_fulfillment_admin');
        $ajaxurl = admin_url('admin-ajax.php');
        ?>
        <script>
        (function() {
            'use strict';
            var nonce   = <?php echo wp_json_encode($nonce); ?>;
            var ajaxurl = <?php echo wp_json_encode($ajaxurl); ?>;
            // Light WooCommerce-green style applied after a successful
            // print fire — matches the server-side `is-printed` initial
            // render so the visual state is stable across reloads.
            var GREEN_STYLE = 'background:#dcefdc;border-color:#7ad03a;color:#1d2327;';

            function markPrinted(btn) {
                if (btn.classList.contains('is-printed')) { return; }
                btn.classList.add('is-printed');
                // Inline style — server uses the same shape, so the
                // after-reload render also lands on the same look.
                btn.setAttribute('style', (btn.getAttribute('style') || '').replace(/;?$/, ';') + GREEN_STYLE);
            }

            function fireClick(e) {
                var btn = e.currentTarget;
                var oid = btn.getAttribute('data-order-id');
                var typ = btn.getAttribute('data-doc-type');
                if (!oid || !typ) { return; }
                btn.disabled = true;
                var fd = new FormData();
                fd.append('action',   'aieo_ff_print_doc_for_order');
                fd.append('nonce',    nonce);
                fd.append('order_id', oid);
                fd.append('doc_type', typ);
                fetch(ajaxurl, { method: 'POST', body: fd, credentials: 'same-origin' })
                    .then(function (r) {
                        // Capture HTTP status — useful when the body is empty
                        // and the only signal is 4xx/5xx.
                        return r.text().then(function (t) { return { status: r.status, body: t }; });
                    })
                    .then(function (resp) {
                        btn.disabled = false;
                        var j = null;
                        try { j = JSON.parse(resp.body); } catch (e) {}
                        if (!j || !j.success) {
                            var msg = (j && j.data && j.data.message)
                                ? j.data.message
                                : ('HTTP ' + resp.status + (resp.body ? (': ' + resp.body.substring(0, 200)) : ''));
                            window.alert('Print failed: ' + msg);
                            return;
                        }
                        markPrinted(btn);
                        if (j.data && j.data.pdf_url) {
                            window.open(j.data.pdf_url, '_blank', 'noopener');
                        }
                    })
                    .catch(function (err) {
                        btn.disabled = false;
                        window.alert('Print error: ' + (err && err.message ? err.message : err));
                    });
            }

            // Delegated binding — orders list might re-render via WC's
            // own JS, so always pick up the latest buttons in the DOM.
            document.addEventListener('click', function (e) {
                var btn = e.target.closest && e.target.closest('.aieo-ff-print-doc');
                if (!btn) { return; }
                e.preventDefault();
                fireClick({ currentTarget: btn });
            });

            // ── Voucher modal ────────────────────────────────────────
            // Single page-level modal (#aieo-ff-voucher-modal) targeted
            // per-row by the "+ Voucher" trigger's data-order-id. Saving
            // POSTs to aieo_ff_save_manual_voucher and re-renders just
            // the affected cell — no page reload.
            //
            // NB: this IIFE runs synchronously at script-parse time,
            // BEFORE admin_footer's later hooks emit the modal HTML.
            // All modal references therefore use a lazy getter (qsm /
            // getModal) so the lookup happens on click, not on load.

            var modalOrderId = null;
            var modalTriggerEl = null;

            function getModal()    { return document.getElementById('aieo-ff-voucher-modal'); }
            function getCarrier()  { return document.getElementById('aieo-ff-voucher-modal-carrier'); }
            function getNoInput()  { return document.getElementById('aieo-ff-voucher-modal-no'); }
            function getNotify()   { return document.getElementById('aieo-ff-voucher-modal-notify'); }
            function getErr()      { return document.getElementById('aieo-ff-voucher-modal-err'); }
            function getCancel()   { return document.getElementById('aieo-ff-voucher-modal-cancel'); }
            function getSave()     { return document.getElementById('aieo-ff-voucher-modal-save'); }

            function openVoucherModal(orderId, triggerEl) {
                var modal = getModal();
                if (!modal) {
                    window.alert('Voucher modal not present in page — please reload.');
                    return;
                }
                modalOrderId   = orderId;
                modalTriggerEl = triggerEl;
                var orderSpan = modal.querySelector('.aieo-ff-voucher-modal__order');
                if (orderSpan) orderSpan.textContent = orderId;
                var noInput = getNoInput();
                if (noInput) noInput.value = '';
                var errEl = getErr();
                if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
                var notifyEl = getNotify();
                if (notifyEl) notifyEl.checked = true;
                // Honour the per-row preselect — server-side resolver
                // maps order.shipping_method.instance_id → carrier code
                // via the AIEO Shipping Rates "Linked carrier" picker.
                var preselect = triggerEl ? triggerEl.getAttribute('data-preselect-carrier') : '';
                var carrierEl = getCarrier();
                if (carrierEl && preselect) {
                    // Only select if the option is actually in the list
                    // (modal dropdown is filtered to linked_carriers()).
                    var match = false;
                    for (var i = 0; i < carrierEl.options.length; i++) {
                        if (carrierEl.options[i].value === preselect) { match = true; break; }
                    }
                    if (match) carrierEl.value = preselect;
                }
                modal.style.display = 'flex';
                if (noInput) setTimeout(function () { noInput.focus(); }, 30);
            }
            function closeVoucherModal() {
                var modal = getModal();
                if (modal) modal.style.display = 'none';
                modalOrderId = null;
                modalTriggerEl = null;
            }

            // Open: any "+ Voucher" row trigger.
            document.addEventListener('click', function (e) {
                var trigger = e.target.closest && e.target.closest('.aieo-ff-voucher-trigger');
                if (!trigger) return;
                e.preventDefault();
                var oid = trigger.getAttribute('data-order-id');
                if (!oid) return;
                openVoucherModal(oid, trigger);
            });
            // Close: backdrop click + Cancel button + Escape key + Save button.
            // All resolved via event delegation so we don't care when the
            // modal HTML lands relative to this script.
            document.addEventListener('click', function (e) {
                if (e.target && e.target.classList && e.target.classList.contains('aieo-ff-voucher-modal__backdrop')) {
                    closeVoucherModal();
                    return;
                }
                if (e.target && (e.target.id === 'aieo-ff-voucher-modal-cancel' || (e.target.closest && e.target.closest('#aieo-ff-voucher-modal-cancel')))) {
                    e.preventDefault();
                    closeVoucherModal();
                    return;
                }
            });
            document.addEventListener('keydown', function (e) {
                if (e.key !== 'Escape') return;
                var modal = getModal();
                if (modal && modal.style.display === 'flex') closeVoucherModal();
            });

            // Save: POST → re-render the row's voucher cell with the
            // returned HTML, optionally surface email-send outcome.
            // Delegated so we don't depend on the modal existing yet.
            document.addEventListener('click', function (e) {
                if (!e.target) return;
                var saveBtn = (e.target.id === 'aieo-ff-voucher-modal-save')
                    ? e.target
                    : (e.target.closest && e.target.closest('#aieo-ff-voucher-modal-save'));
                if (!saveBtn) return;
                e.preventDefault();
                (function (saveBtn) {
                    if (!modalOrderId) { closeVoucherModal(); return; }
                    var carrierEl = getCarrier();
                    var noEl      = getNoInput();
                    var notifyEl  = getNotify();
                    var errEl     = getErr();
                    var carrier = carrierEl ? carrierEl.value : '';
                    var vno     = noEl ? noEl.value.trim() : '';
                    var notify  = notifyEl && notifyEl.checked ? '1' : '';
                    if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
                    if (!carrier || !vno) {
                        if (errEl) { errEl.style.display = 'block'; errEl.textContent = 'Tracking number is required.'; }
                        return;
                    }
                    saveBtn.disabled = true;
                    var fd = new FormData();
                    fd.append('action',     'aieo_ff_save_manual_voucher');
                    fd.append('nonce',      nonce);
                    fd.append('order_id',   modalOrderId);
                    fd.append('carrier',    carrier);
                    fd.append('voucher_no', vno);
                    fd.append('notify',     notify);
                    fetch(ajaxurl, { method: 'POST', body: fd, credentials: 'same-origin' })
                        .then(function (r) { return r.text().then(function (t) { return { status: r.status, body: t }; }); })
                        .then(function (resp) {
                            saveBtn.disabled = false;
                            var j = null;
                            try { j = JSON.parse(resp.body); } catch (e) {}
                            if (!j || !j.success) {
                                var msg = (j && j.data && j.data.message)
                                    ? j.data.message
                                    : ('HTTP ' + resp.status + (resp.body ? (': ' + resp.body.substring(0, 200)) : ''));
                                if (errEl) { errEl.style.display = 'block'; errEl.textContent = msg; }
                                return;
                            }
                            // Replace the original row cell with the freshly
                            // rendered HTML returned by the server.
                            if (modalTriggerEl && j.data && j.data.cell_html) {
                                var cell = modalTriggerEl.closest('td');
                                if (cell) cell.innerHTML = j.data.cell_html;
                            }
                            closeVoucherModal();
                            // Surface email outcome to the operator.
                            if (notify === '1' && j.data) {
                                if (!j.data.email_sent) {
                                    if (j.data.email_err) {
                                        window.alert('Saved, but email failed: ' + j.data.email_err);
                                    } else {
                                        window.alert('Saved, but email was not sent (customer may have no email on file).');
                                    }
                                }
                            }
                        })
                        .catch(function (err) {
                            saveBtn.disabled = false;
                            if (errEl) {
                                errEl.style.display = 'block';
                                errEl.textContent = 'Network error: ' + (err && err.message ? err.message : err);
                            }
                        });
                })(saveBtn);
            });
        })();
        </script>
        <?php
    }

    
    public function register_meta_box($screen_id_or_post_type, $obj) {
        
        $hpos_screen   = function_exists('wc_get_page_screen_id') ? wc_get_page_screen_id('shop-order') : 'woocommerce_page_wc-orders';
        $legacy_screen = 'shop_order';

        if ($screen_id_or_post_type !== $hpos_screen && $screen_id_or_post_type !== $legacy_screen) {
            return;
        }
        add_meta_box(
            'aieo_fulfillment_meta_box',
            __('Order Documents & Couriers', 'ai-eshop-optimizer'),
            array($this, 'render_meta_box'),
            $screen_id_or_post_type,
            'normal',
            'high'
        );
    }

    
    public function render_meta_box($post_or_order) {
        if (!current_user_can('manage_woocommerce')) {
            echo '<p>' . esc_html__('Insufficient permissions.', 'ai-eshop-optimizer') . '</p>';
            return;
        }
        $order = ($post_or_order instanceof WC_Order)
            ? $post_or_order
            : (function_exists('wc_get_order') ? wc_get_order($post_or_order) : null);
        if (!$order) {
            echo '<p>' . esc_html__('Order not found.', 'ai-eshop-optimizer') . '</p>';
            return;
        }
        $order_id = (int) $order->get_id();
        $vouchers = AIEO_Fulfillment_Vouchers::get_for_order($order_id);
        $couriers = $this->couriers();
        $nonce    = wp_create_nonce('aieo_fulfillment_admin');

        echo '<div class="aieo-ff-meta-box" data-order-id="' . esc_attr($order_id) . '" data-nonce="' . esc_attr($nonce) . '">';

        
        echo '<h4 style="margin-top:0;">' . esc_html__('Vouchers', 'ai-eshop-optimizer') . '</h4>';
        if (empty($vouchers)) {
            echo '<p><em>' . esc_html__('No vouchers yet for this order.', 'ai-eshop-optimizer') . '</em></p>';
        } else {
            echo '<table class="widefat striped" style="margin-bottom:12px;"><thead><tr>';
            echo '<th>' . esc_html__('Carrier', 'ai-eshop-optimizer') . '</th>';
            echo '<th>' . esc_html__('Voucher', 'ai-eshop-optimizer') . '</th>';
            echo '<th>' . esc_html__('Status', 'ai-eshop-optimizer') . '</th>';
            echo '<th>' . esc_html__('Created', 'ai-eshop-optimizer') . '</th>';
            echo '<th style="width:340px;">' . esc_html__('Actions', 'ai-eshop-optimizer') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($vouchers as $v) {
                $carrier_code = (string) $v['carrier'];
                $driver       = isset($couriers[$carrier_code]) ? $couriers[$carrier_code] : null;
                $caps         = $driver ? $driver->capabilities() : array();
                $vno          = (string) $v['voucher_no'];
                $tracking_url = $driver ? $driver->tracking_url($vno) : '';
                $carrier_label = $driver ? $driver->label() : strtoupper($carrier_code);
                $created_human = !empty($v['created_at']) ? esc_html(mysql2date(get_option('date_format') . ' ' . get_option('time_format'), $v['created_at'])) : '—';

                echo '<tr data-voucher-id="' . esc_attr((int) $v['id']) . '">';
                echo '<td><strong>' . esc_html($carrier_label) . '</strong>';
                if (!empty($v['carrier_account'])) { echo '<br><small>' . esc_html((string) $v['carrier_account']) . '</small>'; }
                echo '</td>';
                echo '<td>';
                if ($tracking_url !== '') {
                    echo '<a href="' . esc_url($tracking_url) . '" target="_blank" rel="noopener"><code>' . esc_html($vno) . '</code></a>';
                } else {
                    echo '<code>' . esc_html($vno) . '</code>';
                }
                echo '</td>';
                echo '<td><span class="aieo-ff-status aieo-ff-status--' . esc_attr((string) $v['status']) . '">' . esc_html((string) $v['status']) . '</span></td>';
                echo '<td>' . $created_human . '</td>'; 
                echo '<td>'; 
                if (in_array('print', $caps, true)) {
                    echo '<button type="button" class="button button-small" data-aieo-ff-action="print">' . esc_html__('Print', 'ai-eshop-optimizer') . '</button> ';
                }
                if (in_array('track', $caps, true)) {
                    
                    
                    
                    
                    
                    echo '<button type="button" class="button button-small" data-aieo-ff-action="track" data-mode="simple">' . esc_html__('Track', 'ai-eshop-optimizer') . '</button> ';
                    echo '<button type="button" class="button button-small" data-aieo-ff-action="track" data-mode="full" title="' . esc_attr__('Full checkpoint history', 'ai-eshop-optimizer') . '">' . esc_html__('Detailed', 'ai-eshop-optimizer') . '</button> ';
                }
                if (in_array('cancel', $caps, true)) {
                    echo '<button type="button" class="button button-small" data-aieo-ff-action="cancel">' . esc_html__('Cancel', 'ai-eshop-optimizer') . '</button> ';
                }
                echo '<button type="button" class="button button-small button-link-delete" data-aieo-ff-action="delete">' . esc_html__('Remove', 'ai-eshop-optimizer') . '</button>';
                echo '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        }
        echo '<p id="aieo-ff-status" style="font-size:13px;color:#646970;"></p>';

        
        echo '<h4 style="margin-top:18px;">' . esc_html__('Add a voucher', 'ai-eshop-optimizer') . '</h4>';
        
        
        
        
        
        
        
        
        $default_carrier = (string) $this->get_carrier_for_order($order);
        if ($default_carrier === '' || !isset($couriers[$default_carrier])) {
            $default_carrier = (string) AIEO_Fulfillment_Settings::get('default_carrier', '');
        }
        if ($default_carrier === '' || !isset($couriers[$default_carrier])) {
            $default_carrier = (string) array_key_first($couriers);
        }
        
        
        
        
        
        
        $dropdown_couriers = array();
        $active            = $this->active_couriers();
        foreach ($couriers as $code => $driver) {
            if (defined('AIEO_Courier_Driver_Custom::PREFIX') && strpos((string) $code, AIEO_Courier_Driver_Custom::PREFIX) === 0) {
                $dropdown_couriers[$code] = $driver;
                continue;
            }
            if (isset($active[$code])) {
                $dropdown_couriers[$code] = $driver;
            }
        }
        if (empty($dropdown_couriers)) {
            
            
            
            $dropdown_couriers = $couriers;
        }
        
        
        if ($default_carrier === '' || !isset($dropdown_couriers[$default_carrier])) {
            $default_carrier = (string) array_key_first($dropdown_couriers);
        }

        echo '<table class="form-table"><tbody>';
        echo '<tr><th>' . esc_html__('Carrier', 'ai-eshop-optimizer') . '</th><td>';
        echo '<select id="aieo-ff-new-carrier">';
        foreach ($dropdown_couriers as $code => $driver) {
            $mode = method_exists($driver, 'create_mode') ? (string) $driver->create_mode() : 'api';
            echo '<option value="' . esc_attr($code) . '" data-create-mode="' . esc_attr($mode) . '"' . selected($default_carrier, $code, false) . '>' . esc_html($driver->label()) . '</option>';
        }
        echo '</select>';
        echo ' <a href="' . esc_url(admin_url('admin.php?page=' . AIEO_DMM::MENU_SLUG . '&dmm_tab=fulfillment&ff_section=settings')) . '" style="margin-left:8px;font-size:12px;">' . esc_html__('Manage carriers…', 'ai-eshop-optimizer') . '</a>';
        echo '</td></tr>';

        
        echo '<tr class="aieo-ff-provider-field" style="display:none;"><th>' . esc_html__('Provider name', 'ai-eshop-optimizer') . '</th><td>';
        echo '<input type="text" id="aieo-ff-manual-provider" class="regular-text" placeholder="' . esc_attr__('e.g. DHL', 'ai-eshop-optimizer') . '"></td></tr>';

        
        
        
        
        
        
        
        echo '<tr><th>' . esc_html__('Tracking number', 'ai-eshop-optimizer') . '</th><td>';
        echo '<input type="text" id="aieo-ff-manual-vno" class="regular-text">';
        echo '<p class="description aieo-ff-vno-help-api" style="font-size:12px;color:#646970;">'
            . esc_html__('Optional — leave blank to auto-generate via the carrier API. Fill in if you already have a voucher number issued elsewhere (we will skip the API call and just record it).', 'ai-eshop-optimizer')
            . '</p>';
        echo '<p class="description aieo-ff-vno-help-manual" style="font-size:12px;color:#646970;display:none;">'
            . esc_html__('Required — paste the tracking number issued by the courier.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</td></tr>';

        
        echo '<tr class="aieo-ff-api-fields"><th>' . esc_html__('Pieces', 'ai-eshop-optimizer') . '</th><td><input type="number" id="aieo-ff-pieces" min="1" value="1" style="width:80px;"></td></tr>';
        
        
        
        
        
        
        echo '<tr class="aieo-ff-api-fields"><th>' . esc_html__('Weight (kg)', 'ai-eshop-optimizer') . '</th><td><input type="text" inputmode="decimal" id="aieo-ff-weight" value="0.5" style="width:100px;"></td></tr>';
        echo '<tr class="aieo-ff-api-fields"><th>' . esc_html__('COD amount', 'ai-eshop-optimizer') . '</th><td><input type="text" inputmode="decimal" id="aieo-ff-cod" value="0" style="width:120px;"></td></tr>';
        echo '<tr class="aieo-ff-api-fields"><th>' . esc_html__('Services', 'ai-eshop-optimizer') . '</th><td><input type="text" id="aieo-ff-services" class="regular-text" placeholder="' . esc_attr__('e.g. 5Σ (Saturday). COD adds αμ automatically.', 'ai-eshop-optimizer') . '"></td></tr>';
        
        
        
        
        $today_local = function_exists('wp_date') ? wp_date('Y-m-d') : gmdate('Y-m-d');
        echo '<tr class="aieo-ff-api-fields"><th>' . esc_html__('Pickup date', 'ai-eshop-optimizer') . '</th><td>'
            . '<input type="date" id="aieo-ff-received-date" value="' . esc_attr($today_local) . '" style="width:170px;">'
            . ' <span class="description" style="margin-left:6px;font-size:12px;color:#646970;">'
            . esc_html__('When the courier picks the parcel up from you. Defaults to today — change only to pre-schedule.', 'ai-eshop-optimizer')
            . '</span></td></tr>';

        echo '</tbody></table>';
        
        
        
        
        
        echo '<p>';
        echo '<button type="button" class="button button-primary" id="aieo-ff-create-btn" data-label-api="' . esc_attr__('Generate voucher', 'ai-eshop-optimizer') . '" data-label-manual="' . esc_attr__('Save tracking', 'ai-eshop-optimizer') . '">' . esc_html__('Generate voucher', 'ai-eshop-optimizer') . '</button> ';
        echo '<button type="button" class="button" id="aieo-ff-create-complete-btn" data-label-api="' . esc_attr__('Generate voucher + mark order completed', 'ai-eshop-optimizer') . '" data-label-manual="' . esc_attr__('Save tracking + mark order completed', 'ai-eshop-optimizer') . '">' . esc_html__('Generate voucher + mark order completed', 'ai-eshop-optimizer') . '</button>';
        echo ' <span id="aieo-ff-create-status" style="margin-left:10px;font-size:13px;color:#646970;"></span></p>';

        
        if (class_exists('AIEO_Fulfillment_Documents')) {
            $docs = AIEO_Fulfillment_Documents::get_for_order($order_id);
            $doc_classes = $this->documents();
            echo '<hr style="margin:18px 0;">';
            echo '<h4 style="margin-top:0;">' . esc_html__('Documents', 'ai-eshop-optimizer') . '</h4>';
            if (!empty($docs)) {
                echo '<table class="widefat striped" style="margin-bottom:8px;"><thead><tr>';
                echo '<th>' . esc_html__('Type', 'ai-eshop-optimizer') . '</th>';
                echo '<th>' . esc_html__('Number', 'ai-eshop-optimizer') . '</th>';
                echo '<th>' . esc_html__('Issued', 'ai-eshop-optimizer') . '</th>';
                echo '<th style="width:240px;">' . esc_html__('Actions', 'ai-eshop-optimizer') . '</th>';
                echo '</tr></thead><tbody>';
                foreach ($docs as $d) {
                    $label   = isset($doc_classes[$d['type']]) ? $doc_classes[$d['type']]->label() : $d['type'];
                    $issued  = !empty($d['issued_at']) ? esc_html(mysql2date(get_option('date_format'), $d['issued_at'])) : '—';
                    $has_pdf = !empty($d['pdf_path']);
                    echo '<tr data-document-id="' . esc_attr((int) $d['id']) . '">';
                    echo '<td><strong>' . esc_html($label) . '</strong></td>';
                    echo '<td><code>' . esc_html((string) $d['number']) . '</code></td>';
                    echo '<td>' . $issued . '</td>'; 
                    echo '<td>'; 
                    echo '<button type="button" class="button button-small" data-aieo-ff-doc-action="open">' . esc_html($has_pdf ? __('Open PDF', 'ai-eshop-optimizer') : __('Generate PDF', 'ai-eshop-optimizer')) . '</button> ';
                    echo '<button type="button" class="button button-small" data-aieo-ff-doc-action="regenerate">' . esc_html__('Regenerate', 'ai-eshop-optimizer') . '</button> ';
                    echo '<button type="button" class="button button-small button-link-delete" data-aieo-ff-doc-action="delete">' . esc_html__('Delete', 'ai-eshop-optimizer') . '</button>';
                    echo '</td></tr>';
                }
                echo '</tbody></table>';
            } else {
                echo '<p><em>' . esc_html__('No documents generated yet.', 'ai-eshop-optimizer') . '</em></p>';
            }
            echo '<p>';
            foreach ($doc_classes as $type => $doc) {
                echo '<button type="button" class="button" data-aieo-ff-create-doc="' . esc_attr($type) . '">'
                   
                   . esc_html(sprintf(__('Generate %s', 'ai-eshop-optimizer'), $doc->label()))
                   . '</button> ';
            }
            echo '<span id="aieo-ff-doc-status" style="margin-left:8px;font-size:13px;color:#646970;"></span>';
            echo '</p>';

            $engine_class = AIEO_Fulfillment_PDF_Engine::available_class();
            if ($engine_class === '') {
                echo '<p style="color:#b91c1c;font-size:12px;"><em>' . esc_html__('Warning: no DOMPDF library detected. Document generation will fail until Phase 10 vendors a copy. (On Vultr today this is auto-resolved via the WPO core plugin.)', 'ai-eshop-optimizer') . '</em></p>';
            }
        }

        echo '</div>'; 

        
        $this->print_meta_box_js();
    }

    private function print_meta_box_js() {
        $ajax = admin_url('admin-ajax.php');
        ?>
        <script>
        (function(){
            var box = document.querySelector('.aieo-ff-meta-box');
            if (!box) return;
            var orderId = box.getAttribute('data-order-id');
            var nonce   = box.getAttribute('data-nonce');
            var ajaxUrl = '<?php echo esc_js($ajax); ?>';

            function setStatus(id, color, text){
                var el = document.getElementById(id); if (!el) return;
                el.style.color = color; el.textContent = text;
            }
            function post(action, data){
                var fd = new FormData();
                fd.set('action', action); fd.set('nonce', nonce); fd.set('order_id', orderId);
                Object.keys(data || {}).forEach(function(k){ fd.set(k, data[k]); });
                return fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd}).then(function(r){return r.json();});
            }

            // Carrier dropdown — show/hide manual vs api fields by create_mode,
            // and swap the primary + secondary button labels so the operator
            // sees "Save tracking" for no-API carriers and "Generate voucher"
            // for API ones.
            var carrierSel  = document.getElementById('aieo-ff-new-carrier');
            var createBtn   = document.getElementById('aieo-ff-create-btn');
            var completeBtn = document.getElementById('aieo-ff-create-complete-btn');
            function refreshFields(){
                if (!carrierSel) return;
                var opt = carrierSel.options[carrierSel.selectedIndex];
                var mode = opt ? (opt.getAttribute('data-create-mode') || 'api') : 'api';
                var isManual = mode === 'manual';
                var isManualOnly = isManual && carrierSel.value === 'manual';
                // Tracking-number row is always visible now; api-only
                // helper fields (pieces / weight / cod / services /
                // pickup date) still hide for manual carriers.
                document.querySelectorAll('.aieo-ff-provider-field').forEach(function(r){ r.style.display = isManualOnly ? '' : 'none'; });
                document.querySelectorAll('.aieo-ff-api-fields').forEach(function(r){ r.style.display = isManual ? 'none' : ''; });
                // Swap the help-text under the tracking-number input —
                // 'optional' wording for API carriers, 'required' for
                // manual ones.
                document.querySelectorAll('.aieo-ff-vno-help-api').forEach(function(p){ p.style.display = isManual ? 'none' : ''; });
                document.querySelectorAll('.aieo-ff-vno-help-manual').forEach(function(p){ p.style.display = isManual ? '' : 'none'; });
                // Swap button labels per mode.
                [createBtn, completeBtn].forEach(function(b){
                    if (!b) return;
                    b.textContent = isManual ? (b.dataset.labelManual || b.textContent) : (b.dataset.labelApi || b.textContent);
                });
            }
            carrierSel && carrierSel.addEventListener('change', refreshFields);
            refreshFields();

            // Shared submit — markComplete=true also POSTs mark_completed=1
            // so the server transitions the order to 'completed' after the
            // voucher saves (which fires WC's completed-order email).
            function submitVoucher(markComplete) {
                var carrier = carrierSel.value;
                var opt = carrierSel.options[carrierSel.selectedIndex];
                var mode = opt ? (opt.getAttribute('data-create-mode') || 'api') : 'api';
                var data = { carrier: carrier };
                var typedVno = (document.getElementById('aieo-ff-manual-vno').value || '').trim();
                if (mode === 'manual') {
                    data.manual_voucher_no = typedVno;
                    if (carrier === 'manual') {
                        data.carrier_account = (document.getElementById('aieo-ff-manual-provider').value || '').trim();
                    }
                    if (!data.manual_voucher_no) { setStatus('aieo-ff-create-status', '#b91c1c', '<?php echo esc_js(__('Tracking number required.', 'ai-eshop-optimizer')); ?>'); return; }
                } else {
                    // API mode — if the operator typed a voucher number,
                    // pass it through so the server skips the API call
                    // and just records the typed number. Otherwise send
                    // the create payload as before.
                    if (typedVno !== '') { data.manual_voucher_no = typedVno; }
                    // Locale-tolerant decimal parser. Accepts either
                    // dot or comma as the decimal mark. When BOTH show
                    // up (thousand-grouped input), the one that appears
                    // LAST is treated as the decimal mark and the other
                    // is stripped as a thousand separator:
                    //   "24,30"     → "24.30"   (European decimal)
                    //   "24.30"     → "24.30"   (universal)
                    //   "1.234,56"  → "1234.56" (European w/ thousand)
                    //   "1,234.56"  → "1234.56" (US w/ thousand)
                    //   "1 234,56"  → "1234.56" (space as thousand)
                    // The backend applies the same rule defensively.
                    function decimalize(s){
                        s = String(s || '').trim().replace(/\s+/g, '');
                        if (s === '') return s;
                        var lc = s.lastIndexOf(','), ld = s.lastIndexOf('.');
                        if (lc > ld)      return s.replace(/\./g, '').replace(',', '.');
                        if (ld > lc)      return s.replace(/,/g, '');
                        return s;
                    }
                    data.pieces       = (document.getElementById('aieo-ff-pieces').value || '1');
                    data.weight       = decimalize(document.getElementById('aieo-ff-weight').value || '0.5');
                    data.cod_amount   = decimalize(document.getElementById('aieo-ff-cod').value || '0');
                    data.services_csv = (document.getElementById('aieo-ff-services').value || '').trim();
                    var rd = (document.getElementById('aieo-ff-received-date').value || '').trim();
                    if (rd) { data.received_date = rd + 'T09:00:00'; }
                }
                if (markComplete) { data.mark_completed = '1'; }
                if (createBtn)   { createBtn.disabled   = true; }
                if (completeBtn) { completeBtn.disabled = true; }
                setStatus('aieo-ff-create-status', '#646970',
                    (mode === 'manual' || typedVno !== '')
                        ? '<?php echo esc_js(__('Saving…', 'ai-eshop-optimizer')); ?>'
                        : '<?php echo esc_js(__('Generating…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_create_voucher', data).then(function(j){
                    if (createBtn)   { createBtn.disabled   = false; }
                    if (completeBtn) { completeBtn.disabled = false; }
                    if (j && j.success) {
                        setStatus('aieo-ff-create-status', '#15803d', (j.data && j.data.message) || '<?php echo esc_js(__('Saved.', 'ai-eshop-optimizer')); ?>');
                        setTimeout(function(){ location.reload(); }, 600);
                    } else {
                        setStatus('aieo-ff-create-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            }
            createBtn   && createBtn.addEventListener('click',   function(){ submitVoucher(false); });
            completeBtn && completeBtn.addEventListener('click', function(){ submitVoucher(true);  });

            // Suppress Enter-as-submit inside this meta box. WP wraps the
            // whole order-edit screen in a single <form>, so pressing
            // Enter in our tracking-number input would submit that form
            // (focus jumped to the Downloadable-products search at the
            // bottom of the page). Instead, intercept Enter and route it
            // to "Save tracking" / "Generate voucher" if the field is
            // ready, else just swallow it.
            box.addEventListener('keydown', function(e){
                if (e.key !== 'Enter') return;
                var t = e.target;
                if (!t || (t.tagName !== 'INPUT' && t.tagName !== 'SELECT')) return;
                if (t.type === 'submit' || t.type === 'button') return;
                e.preventDefault();
                e.stopPropagation();
                // Convenience: if focus is in the Add-Voucher area, treat Enter as "Save tracking".
                if (t.id === 'aieo-ff-manual-vno' || t.id === 'aieo-ff-pieces' || t.id === 'aieo-ff-weight' || t.id === 'aieo-ff-cod' || t.id === 'aieo-ff-services' || t.id === 'aieo-ff-manual-provider') {
                    if (createBtn && !createBtn.disabled) { submitVoucher(false); }
                }
            });

            // Per-row actions.
            box.addEventListener('click', function(e){
                var btn = e.target.closest('[data-aieo-ff-action]'); if (!btn) return;
                var row = btn.closest('tr[data-voucher-id]'); if (!row) return;
                var id  = row.getAttribute('data-voucher-id');
                var act = btn.getAttribute('data-aieo-ff-action');
                btn.disabled = true;
                setStatus('aieo-ff-status', '#646970', '<?php echo esc_js(__('Working…', 'ai-eshop-optimizer')); ?>');
                if (act === 'print') {
                    post('aieo_ff_get_label', {voucher_id: id}).then(function(j){
                        btn.disabled = false;
                        if (j && j.success && j.data && j.data.pdf_url) {
                            window.open(j.data.pdf_url, '_blank', 'noopener');
                            setStatus('aieo-ff-status', '#15803d', '<?php echo esc_js(__('Label opened.', 'ai-eshop-optimizer')); ?>');
                        } else {
                            setStatus('aieo-ff-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Print failed.', 'ai-eshop-optimizer')); ?>');
                        }
                    });
                } else if (act === 'track') {
                    var mode = btn.getAttribute('data-mode') || 'simple';
                    post('aieo_ff_track_now', {voucher_id: id, mode: mode}).then(function(j){
                        btn.disabled = false;
                        if (j && j.success) {
                            setStatus('aieo-ff-status', '#15803d', (j.data && j.data.message) || '<?php echo esc_js(__('Tracking refreshed.', 'ai-eshop-optimizer')); ?>');
                            setTimeout(function(){ location.reload(); }, 500);
                        } else {
                            setStatus('aieo-ff-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Track failed.', 'ai-eshop-optimizer')); ?>');
                        }
                    });
                } else if (act === 'cancel') {
                    if (!confirm('<?php echo esc_js(__('Cancel this voucher? This calls the carrier API.', 'ai-eshop-optimizer')); ?>')) { btn.disabled=false; return; }
                    post('aieo_ff_cancel_voucher', {voucher_id: id}).then(function(j){
                        btn.disabled = false;
                        if (j && j.success) { setStatus('aieo-ff-status', '#15803d', '<?php echo esc_js(__('Canceled.', 'ai-eshop-optimizer')); ?>'); setTimeout(function(){ location.reload(); }, 500); }
                        else { setStatus('aieo-ff-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Cancel failed.', 'ai-eshop-optimizer')); ?>'); }
                    });
                } else if (act === 'delete') {
                    if (!confirm('<?php echo esc_js(__('Remove this voucher row from this site only? Does NOT cancel at carrier.', 'ai-eshop-optimizer')); ?>')) { btn.disabled=false; return; }
                    post('aieo_ff_delete_voucher', {voucher_id: id}).then(function(j){
                        btn.disabled = false;
                        if (j && j.success) { row.remove(); setStatus('aieo-ff-status', '#15803d', '<?php echo esc_js(__('Removed.', 'ai-eshop-optimizer')); ?>'); }
                        else { setStatus('aieo-ff-status', '#b91c1c', '<?php echo esc_js(__('Remove failed.', 'ai-eshop-optimizer')); ?>'); }
                    });
                }
            });

            // Documents — Generate Invoice / Generate Packing Slip buttons.
            box.addEventListener('click', function(e){
                var btn = e.target.closest('[data-aieo-ff-create-doc]'); if (!btn) return;
                var type = btn.getAttribute('data-aieo-ff-create-doc');
                btn.disabled = true;
                setStatus('aieo-ff-doc-status', '#646970', '<?php echo esc_js(__('Generating PDF…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_create_document', {doc_type: type}).then(function(j){
                    btn.disabled = false;
                    if (j && j.success) {
                        setStatus('aieo-ff-doc-status', '#15803d', (j.data && j.data.message) || '<?php echo esc_js(__('Document generated.', 'ai-eshop-optimizer')); ?>');
                        if (j.data && j.data.pdf_url) { window.open(j.data.pdf_url, '_blank', 'noopener'); }
                        setTimeout(function(){ location.reload(); }, 700);
                    } else {
                        setStatus('aieo-ff-doc-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Generate failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });

            // Per-document row actions: open / regenerate / delete.
            box.addEventListener('click', function(e){
                var btn = e.target.closest('[data-aieo-ff-doc-action]'); if (!btn) return;
                var row = btn.closest('tr[data-document-id]'); if (!row) return;
                var id  = row.getAttribute('data-document-id');
                var act = btn.getAttribute('data-aieo-ff-doc-action');
                btn.disabled = true;
                setStatus('aieo-ff-doc-status', '#646970', '<?php echo esc_js(__('Working…', 'ai-eshop-optimizer')); ?>');
                if (act === 'open') {
                    post('aieo_ff_get_document_pdf', {document_id: id, regenerate: 0}).then(function(j){
                        btn.disabled = false;
                        if (j && j.success && j.data && j.data.pdf_url) {
                            window.open(j.data.pdf_url, '_blank', 'noopener');
                            setStatus('aieo-ff-doc-status', '#15803d', '<?php echo esc_js(__('Opened.', 'ai-eshop-optimizer')); ?>');
                        } else {
                            setStatus('aieo-ff-doc-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Open failed.', 'ai-eshop-optimizer')); ?>');
                        }
                    });
                } else if (act === 'regenerate') {
                    post('aieo_ff_get_document_pdf', {document_id: id, regenerate: 1}).then(function(j){
                        btn.disabled = false;
                        if (j && j.success && j.data && j.data.pdf_url) {
                            window.open(j.data.pdf_url, '_blank', 'noopener');
                            setStatus('aieo-ff-doc-status', '#15803d', '<?php echo esc_js(__('Regenerated.', 'ai-eshop-optimizer')); ?>');
                        } else {
                            setStatus('aieo-ff-doc-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Regenerate failed.', 'ai-eshop-optimizer')); ?>');
                        }
                    });
                } else if (act === 'delete') {
                    if (!confirm('<?php echo esc_js(__('Delete this document and its PDF file?', 'ai-eshop-optimizer')); ?>')) { btn.disabled=false; return; }
                    post('aieo_ff_delete_document', {document_id: id}).then(function(j){
                        btn.disabled = false;
                        if (j && j.success) { row.remove(); setStatus('aieo-ff-doc-status', '#15803d', '<?php echo esc_js(__('Deleted.', 'ai-eshop-optimizer')); ?>'); }
                        else { setStatus('aieo-ff-doc-status', '#b91c1c', '<?php echo esc_js(__('Delete failed.', 'ai-eshop-optimizer')); ?>'); }
                    });
                }
            });
        })();
        </script>
        <?php
    }

    

    private function verify_admin_ajax() {
        if (!current_user_can('manage_woocommerce')) {
            wp_send_json_error(array('message' => 'permission_denied'), 403);
        }
        check_ajax_referer('aieo_fulfillment_admin', 'nonce');
    }

    public function ajax_create_voucher() {
        $this->verify_admin_ajax();

        $order_id = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0;
        $carrier  = isset($_POST['carrier'])  ? sanitize_key((string) wp_unslash($_POST['carrier'])) : '';
        if ($order_id <= 0 || $carrier === '') {
            wp_send_json_error(array('message' => 'order_id and carrier required'));
        }

        $order = function_exists('wc_get_order') ? wc_get_order($order_id) : null;
        if (!$order) { wp_send_json_error(array('message' => 'order_not_found')); }

        $couriers = $this->couriers();
        if (!isset($couriers[$carrier])) {
            wp_send_json_error(array('message' => 'unknown_carrier'));
        }
        $driver = $couriers[$carrier];

        
        
        
        
        $create_mode = method_exists($driver, 'create_mode') ? (string) $driver->create_mode() : 'api';
        $typed_vno = isset($_POST['manual_voucher_no']) ? sanitize_text_field((string) wp_unslash($_POST['manual_voucher_no'])) : '';

        
        
        
        
        
        
        
        $use_manual_entry = ($create_mode === 'manual') || ($typed_vno !== '');
        if ($use_manual_entry) {
            $vno = $typed_vno;
            if ($vno === '') { wp_send_json_error(array('message' => 'tracking_number_required')); }

            $shipping_paid = (float) $order->get_shipping_total();
            $row = array(
                'order_id'               => $order_id,
                'carrier'                => $carrier,
                'voucher_no'             => $vno,
                'status'                 => 'created',
                'status_at'              => current_time('mysql', true),
                'customer_shipping_paid' => $shipping_paid,
            );
            
            if ($carrier === 'manual') {
                $row['carrier_account'] = isset($_POST['carrier_account']) ? sanitize_text_field((string) wp_unslash($_POST['carrier_account'])) : '';
            }

            $stored = AIEO_Fulfillment_Vouchers::create($row);
            if (empty($stored['ok'])) {
                wp_send_json_error(array('message' => 'db_insert_failed: ' . (isset($stored['error']) ? $stored['error'] : '')));
            }

            $message = sprintf(
                
                __('Tracking number %s saved.', 'ai-eshop-optimizer'),
                $vno
            );

            $marked_complete = false;
            if (!empty($_POST['mark_completed'])) {
                $current = (string) $order->get_status();
                if (!in_array($current, array('completed', 'refunded', 'cancelled', 'failed', 'trash'), true)) {
                    $order->update_status('completed', __('Marked completed by AIEO after saving tracking number.', 'ai-eshop-optimizer'));
                    $marked_complete = true;
                    $message .= ' ' . __('Order marked completed; customer notification sent.', 'ai-eshop-optimizer');
                } else {
                    $message .= ' ' . sprintf(
                        
                        __('Order already %s — skipped status change.', 'ai-eshop-optimizer'),
                        $current
                    );
                }
            }

            wp_send_json_success(array(
                'voucher_id'      => (int) $stored['id'],
                'voucher_no'      => $vno,
                'marked_complete' => $marked_complete,
                'message'         => $message,
            ));
        }

        $payload = $this->build_voucher_payload($order);
        if ($carrier === 'manual') {
            
            
            $payload['manual_voucher_no'] = isset($_POST['manual_voucher_no']) ? sanitize_text_field((string) wp_unslash($_POST['manual_voucher_no'])) : '';
            $payload['carrier_account']   = isset($_POST['carrier_account'])   ? sanitize_text_field((string) wp_unslash($_POST['carrier_account']))   : '';
        } else {
            $weight_raw = isset($_POST['weight'])     ? (string) wp_unslash($_POST['weight'])     : '';
            $cod_raw    = isset($_POST['cod_amount']) ? (string) wp_unslash($_POST['cod_amount']) : '';
            $payload['pieces']       = isset($_POST['pieces']) ? max(1, (int) $_POST['pieces']) : 1;
            $payload['weight']       = self::parse_locale_decimal($weight_raw, 0.5);
            $payload['cod_amount']   = self::parse_locale_decimal($cod_raw,    0.0);
            $payload['services_csv'] = isset($_POST['services_csv']) ? sanitize_text_field((string) wp_unslash($_POST['services_csv'])) : '';
            
            
            
            if (isset($_POST['received_date'])) {
                $rd = sanitize_text_field((string) wp_unslash($_POST['received_date']));
                if ($rd !== '') { $payload['received_date'] = $rd; }
            }
        }

        $result = $driver->create_voucher($payload);
        if (empty($result['ok'])) {
            wp_send_json_error(array('message' => isset($result['error']) ? $result['error'] : 'create_failed'));
        }

        $voucher_no = (string) ($result['voucher_no'] ?? '');
        if ($voucher_no === '') { wp_send_json_error(array('message' => 'no_voucher_number_returned')); }

        $row = array(
            'order_id'   => $order_id,
            'carrier'    => $carrier,
            'voucher_no' => $voucher_no,
            'pieces'     => $payload['pieces'] ?? 1,
            'weight'     => $payload['weight'] ?? null,
            'cod_amount' => $payload['cod_amount'] ?? null,
            'services_csv' => $payload['services_csv'] ?? '',
            'reference_1'  => $payload['reference_1'] ?? '',
            'job_id'       => isset($result['job_id']) ? (int) $result['job_id'] : null,
            'status'       => 'created',
            
            
            
            'customer_shipping_paid' => isset($payload['customer_shipping_paid'])
                ? (float) $payload['customer_shipping_paid'] : null,
        );
        if ($carrier === 'manual' && !empty($result['carrier_account'])) {
            $row['carrier_account'] = (string) $result['carrier_account'];
        }

        
        
        
        
        
        if (method_exists($driver, 'has_capability') && $driver->has_capability('calculate_cost')) {
            $cost_r = $driver->calculate_cost($payload);
            if (!empty($cost_r['ok'])) {
                $row['carrier_cost'] = isset($cost_r['cost']) ? (float) $cost_r['cost'] : null;
                $row['carrier_vat']  = isset($cost_r['vat'])  ? (float) $cost_r['vat']  : null;
                $row['cost_source']  = 'quoted';
            }
        }

        $stored = AIEO_Fulfillment_Vouchers::create($row);
        if (empty($stored['ok'])) {
            wp_send_json_error(array('message' => 'db_insert_failed: ' . (isset($stored['error']) ? $stored['error'] : '')));
        }

        
        if (!empty($result['subvouchers']) && is_array($result['subvouchers'])) {
            foreach ($result['subvouchers'] as $sub) {
                AIEO_Fulfillment_Vouchers::create(array(
                    'order_id'          => $order_id,
                    'carrier'           => $carrier,
                    'voucher_no'        => (string) $sub,
                    'parent_voucher_no' => $voucher_no,
                    'status'            => 'created',
                ));
            }
        }

        $message = sprintf(
            
            __('Voucher %s created.', 'ai-eshop-optimizer'),
            $voucher_no
        );

        
        
        
        
        
        $marked_complete = false;
        if (!empty($_POST['mark_completed'])) {
            $current = (string) $order->get_status();
            if (!in_array($current, array('completed', 'refunded', 'cancelled', 'failed', 'trash'), true)) {
                $order->update_status('completed', __('Marked completed by AIEO after voucher creation.', 'ai-eshop-optimizer'));
                $marked_complete = true;
                $message .= ' ' . __('Order marked completed; customer notification sent.', 'ai-eshop-optimizer');
            } else {
                $message .= ' ' . sprintf(
                    
                    __('Order already %s — skipped status change.', 'ai-eshop-optimizer'),
                    $current
                );
            }
        }

        wp_send_json_success(array(
            'voucher_id'      => (int) $stored['id'],
            'voucher_no'      => $voucher_no,
            'marked_complete' => $marked_complete,
            'message'         => $message,
        ));
    }

    public function ajax_save_manual() {
        
        $_POST['carrier'] = 'manual';
        $this->ajax_create_voucher();
    }

    public function ajax_get_label() {
        $this->verify_admin_ajax();
        $voucher_id = isset($_POST['voucher_id']) ? (int) $_POST['voucher_id'] : 0;
        $voucher = AIEO_Fulfillment_Vouchers::get($voucher_id);
        if (!$voucher) { wp_send_json_error(array('message' => 'voucher_not_found')); }

        $couriers = $this->couriers();
        $carrier  = (string) $voucher['carrier'];
        $driver   = isset($couriers[$carrier]) ? $couriers[$carrier] : null;
        if (!$driver || !$driver->has_capability('print')) {
            wp_send_json_error(array('message' => 'driver_no_print_capability'));
        }
        $r = $driver->get_label_pdf((string) $voucher['voucher_no'], 'A4');
        if (empty($r['ok'])) {
            wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'print_failed'));
        }

        
        if (!empty($r['pdf_url'])) {
            AIEO_Fulfillment_Vouchers::mark_printed($voucher_id);
            wp_send_json_success(array('pdf_url' => $r['pdf_url']));
        }

        
        if (!empty($r['pdf']) && is_string($r['pdf'])) {
            $cached = $this->cache_label_pdf((int) $voucher['order_id'], $voucher_id, $carrier, (string) $voucher['voucher_no'], $r['pdf']);
            if ($cached) {
                AIEO_Fulfillment_Vouchers::mark_printed($voucher_id);
                wp_send_json_success(array('pdf_url' => $cached));
            }
        }
        wp_send_json_error(array('message' => 'no_pdf_returned'));
    }

    public function ajax_cancel_voucher() {
        $this->verify_admin_ajax();
        $voucher_id = isset($_POST['voucher_id']) ? (int) $_POST['voucher_id'] : 0;
        $voucher = AIEO_Fulfillment_Vouchers::get($voucher_id);
        if (!$voucher) { wp_send_json_error(array('message' => 'voucher_not_found')); }

        $couriers = $this->couriers();
        $carrier  = (string) $voucher['carrier'];
        $driver   = isset($couriers[$carrier]) ? $couriers[$carrier] : null;
        if (!$driver || !$driver->has_capability('cancel')) {
            wp_send_json_error(array('message' => 'driver_no_cancel_capability'));
        }

        
        $r = ($carrier === 'geniki' && method_exists($driver, 'cancel_by_job') && !empty($voucher['job_id']))
            ? $driver->cancel_by_job((int) $voucher['job_id'])
            : $driver->cancel((string) $voucher['voucher_no']);

        if (empty($r['ok'])) {
            wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'cancel_failed'));
        }
        AIEO_Fulfillment_Vouchers::update($voucher_id, array(
            'status'      => 'canceled',
            'canceled_at' => current_time('mysql', true),
        ));
        wp_send_json_success(array('message' => __('Canceled.', 'ai-eshop-optimizer')));
    }

    public function ajax_track_now() {
        $this->verify_admin_ajax();
        $voucher_id = isset($_POST['voucher_id']) ? (int) $_POST['voucher_id'] : 0;
        $voucher = AIEO_Fulfillment_Vouchers::get($voucher_id);
        if (!$voucher) { wp_send_json_error(array('message' => 'voucher_not_found')); }

        $couriers = $this->couriers();
        $carrier  = (string) $voucher['carrier'];
        $driver   = isset($couriers[$carrier]) ? $couriers[$carrier] : null;
        if (!$driver || !$driver->has_capability('track')) {
            wp_send_json_error(array('message' => 'driver_no_track_capability'));
        }

        
        
        
        
        $mode = isset($_POST['mode']) && sanitize_key((string) wp_unslash($_POST['mode'])) === 'simple' ? 'simple' : 'full';
        $r = ($mode === 'simple')
            ? $driver->track_simple((string) $voucher['voucher_no'])
            : $driver->track((string) $voucher['voucher_no']);
        if (empty($r['ok'])) {
            wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'track_failed'));
        }

        $code_str = (string) ($r['status_code'] ?? '');
        $unified_status = $driver->map_status($code_str);
        
        
        
        if ($unified_status === 'unknown' && !empty($r['delivered_at'])) {
            $unified_status = 'delivered';
        }
        AIEO_Fulfillment_Vouchers::update_status(
            $voucher_id,
            $unified_status !== 'unknown' ? $unified_status : (string) $voucher['status'],
            $code_str,
            isset($r['delivered_at']) ? $r['delivered_at'] : null,
            isset($r['consignee']) ? $r['consignee'] : null,
            isset($r['arrived_at']) ? $r['arrived_at'] : null,
            isset($r['arrived_status']) ? $r['arrived_status'] : null
        );
        $checkpoint_count = 0;
        if (!empty($r['checkpoints'])) {
            AIEO_Fulfillment_Vouchers::record_checkpoints($voucher_id, $r['checkpoints']);
            $checkpoint_count = count($r['checkpoints']);
        }
        wp_send_json_success(array(
            'status'       => $unified_status,
            'mode'         => $mode,
            'delivered_at' => !empty($r['delivered_at']) ? gmdate('Y-m-d H:i:s', (int) $r['delivered_at']) : null,
            'consignee'    => isset($r['consignee']) ? $r['consignee'] : null,
            'checkpoints'  => $checkpoint_count,
            'message'      => $mode === 'simple'
                
                ? sprintf(__('Status: %s · delivery date refreshed', 'ai-eshop-optimizer'), $unified_status)
                
                : sprintf(__('Status: %1$s · checkpoints: %2$d', 'ai-eshop-optimizer'), $unified_status, $checkpoint_count),
        ));
    }

    
    public function ajax_backfill_delivered_batch() {
        $this->verify_admin_ajax();

        $carrier = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : 'geniki';
        $offset  = isset($_POST['offset'])  ? max(0, (int) $_POST['offset']) : 0;
        $batch   = isset($_POST['batch'])   ? max(1, min(50, (int) $_POST['batch'])) : 10;

        $couriers = $this->couriers();
        if (!isset($couriers[$carrier])) {
            wp_send_json_error(array('message' => 'unknown_carrier'));
        }
        $driver = $couriers[$carrier];
        if (!$driver->has_capability('track')) {
            wp_send_json_error(array('message' => 'driver_no_track_capability'));
        }

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        
        
        
        
        
        
        $terminal_not_found_codes = array(9);

        
        
        
        
        
        
        $consec_in  = isset($_POST['consecutive_errors']) ? max(0, (int) $_POST['consecutive_errors']) : 0;
        $consec_max = 5;
        $consec     = $consec_in;
        $aborted    = false;
        $sweep_mode = !empty($_POST['sweep']);

        
        
        $total = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table}
             WHERE carrier = %s
               AND delivered_at IS NULL
               AND status NOT IN ('canceled','returned','untrackable')",
            $carrier
        ));

        
        
        
        
        
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, voucher_no FROM {$table}
             WHERE carrier = %s
               AND delivered_at IS NULL
               AND status NOT IN ('canceled','returned','untrackable')
             ORDER BY id DESC
             LIMIT %d",
            $carrier,
            $batch
        ), ARRAY_A);

        $delivered = 0;
        $still_pending = 0;
        $marked_untrackable = 0;
        $errors = array();

        foreach ((array) $rows as $row) {
            
            if (!$sweep_mode && $consec >= $consec_max) { $aborted = true; break; }

            $vid = (int) $row['id'];
            $vno = (string) $row['voucher_no'];
            if ($vno === '') { continue; }

            $r = $driver->track_simple($vno);
            if (empty($r['ok'])) {
                $code_int   = isset($r['code']) ? (int) $r['code'] : 0;
                $is_terminal = in_array($code_int, $terminal_not_found_codes, true);
                if ($is_terminal) {
                    
                    
                    
                    
                    
                    
                    
                    AIEO_Fulfillment_Vouchers::update($vid, array(
                        'status'         => 'untrackable',
                        'status_code'    => 'CARRIER_NOT_FOUND_' . $code_int,
                        'status_at'      => current_time('mysql', true),
                        'last_polled_at' => current_time('mysql', true),
                    ));
                    $marked_untrackable++;
                } else {
                    
                    $errors[] = $vno . ': ' . (isset($r['error']) ? (string) $r['error'] : 'track_failed');
                    AIEO_Fulfillment_Vouchers::update($vid, array(
                        'last_polled_at' => current_time('mysql', true),
                    ));
                }
                $consec++;
                if (!$sweep_mode && $consec >= $consec_max) { $aborted = true; break; }
                continue;
            }
            $consec = 0;
            if (!empty($r['delivered_at'])) {
                AIEO_Fulfillment_Vouchers::update_status(
                    $vid,
                    'delivered',
                    (string) ($r['status_code'] ?? ''),
                    (int) $r['delivered_at'],
                    isset($r['consignee']) ? (string) $r['consignee'] : null
                );
                $delivered++;
            } else {
                AIEO_Fulfillment_Vouchers::update($vid, array(
                    'last_polled_at' => current_time('mysql', true),
                ));
                $still_pending++;
            }
        }

        $processed_this_batch = is_array($rows) ? count($rows) : 0;
        $complete = ($processed_this_batch === 0) || $aborted;

        wp_send_json_success(array(
            'processed'           => $processed_this_batch,
            'delivered'           => $delivered,
            'pending'             => $still_pending,
            'untrackable'         => $marked_untrackable,
            'errors'              => array_slice($errors, 0, 10),
            'remaining'           => max(0, $total - $processed_this_batch),
            'complete'            => $complete,
            'consecutive_errors'  => $consec,
            'aborted_consecutive' => $aborted,
        ));
    }

    
    private static function bucket_where_sql($bucket) {
        switch ((string) $bucket) {
            case '2':       return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <= 48';
            case '3':       return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) >  48 AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <=  72';
            case '4':       return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) >  72 AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <=  96';
            case '5':       return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) >  96 AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <= 120';
            case '6':       return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) > 120 AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <= 144';
            case '7':       return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) > 144 AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <= 168';
            case '8':       return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) > 168 AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <= 192';
            case '9':       return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) > 192 AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <= 216';
            case '10':      return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) > 216 AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) <= 240';
            case 'over_10': return 'TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) > 240';
        }
        return '';
    }

    
    public function ajax_metrics_drill() {
        $this->verify_admin_ajax();

        $prefix     = isset($_POST['prefix']) ? (string) wp_unslash($_POST['prefix']) : '';
        $prefix_len = isset($_POST['prefix_len']) ? (int) $_POST['prefix_len'] : 2;
        if ($prefix_len < 1 || $prefix_len > 5) { $prefix_len = 2; }
        
        
        $prefix = preg_replace('/[^0-9A-Za-z]/', '', $prefix);
        if ($prefix !== '' && strlen($prefix) > $prefix_len) {
            wp_send_json_error(array('message' => 'invalid prefix'));
        }
        $carrier = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : '';
        $bucket  = isset($_POST['bucket'])  ? sanitize_key((string) wp_unslash($_POST['bucket']))  : '';
        
        
        
        if ($prefix === '' && $bucket === '') {
            wp_send_json_error(array('message' => 'either prefix or bucket required'));
        }

        global $wpdb;
        $vt = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $ot = $wpdb->prefix . 'wc_orders';
        $at = $wpdb->prefix . 'wc_order_addresses';

        $where_clauses = array();
        $params        = array();
        if ($prefix !== '') {
            $where_clauses[] = "LEFT(REPLACE(CAST(a.postcode AS CHAR), ' ', ''), {$prefix_len}) = %s";
            $params[]        = $prefix;
        }
        
        $bucket_sql = self::bucket_where_sql($bucket);
        if ($bucket_sql !== '') {
            $where_clauses[] = $bucket_sql;
        }
        $where_carrier = '';
        if ($carrier !== '') {
            $where_carrier = 'AND v.carrier = %s';
            $params[]      = $carrier;
        }

        
        
        
        
        $where_extra = empty($where_clauses) ? '' : ' AND (' . implode(') AND (', $where_clauses) . ')';
        $sanity_where = self::delivery_metrics_sanity_where();
        
        $sql = "
            SELECT
                v.order_id,
                v.carrier,
                v.voucher_no,
                v.delivered_at,
                a.first_name,
                a.last_name,
                a.address_1,
                a.address_2,
                a.city,
                a.postcode,
                a.country,
                o.total_amount,
                TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) AS h_order_to_delivery
            FROM {$vt} v
            INNER JOIN {$ot} o ON o.id = v.order_id
            INNER JOIN {$at} a ON a.order_id = v.order_id AND a.address_type = 'shipping'
            WHERE v.delivered_at IS NOT NULL
              AND a.postcode     IS NOT NULL
              AND a.postcode     <> ''
              {$sanity_where}
              {$where_extra}
              {$where_carrier}
            ORDER BY v.delivered_at DESC, v.id DESC
            LIMIT 250
        ";
        $rows = empty($params) ? $wpdb->get_results($sql, ARRAY_A) : $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);

        $couriers = $this->couriers();
        $carrier_labels = array();
        foreach ($couriers as $code => $driver) {
            $carrier_labels[$code] = $driver->label();
        }

        $fmt_money = static function ($v) {
            if ($v === null || $v === '') { return ''; }
            return '€' . number_format((float) $v, 2, '.', ',');
        };
        $fmt_hours_days = static function ($v) {
            if ($v === null || $v === '') { return ''; }
            $h = (float) $v;
            return sprintf('%.1f h (%.1f d)', $h, $h / 24);
        };

        $out = array();
        foreach ((array) $rows as $r) {
            $vno = (string) $r['voucher_no'];
            $carrier_code = (string) $r['carrier'];
            $driver = isset($couriers[$carrier_code]) ? $couriers[$carrier_code] : null;
            $tracking_url = $driver ? $driver->tracking_url($vno) : '';

            
            
            
            $addr_parts = array_filter(array(
                trim((string) $r['address_1']),
                trim((string) $r['address_2']),
                trim((string) $r['city']),
                trim((string) $r['postcode']),
                trim((string) $r['country']),
            ), 'strlen');

            $name = trim(((string) $r['first_name']) . ' ' . ((string) $r['last_name']));

            $out[] = array(
                'order_id'        => (int) $r['order_id'],
                'order_admin_url' => admin_url('admin.php?page=wc-orders&action=edit&id=' . (int) $r['order_id']),
                'carrier'         => $carrier_code,
                'carrier_label'   => isset($carrier_labels[$carrier_code]) ? $carrier_labels[$carrier_code] : strtoupper($carrier_code),
                'voucher_no'      => $vno,
                'tracking_url'    => $tracking_url,
                'customer_name'   => $name,
                'address'         => implode(', ', $addr_parts),
                'order_total'     => $fmt_money($r['total_amount']),
                'order_to_delivery' => $fmt_hours_days($r['h_order_to_delivery']),
            );
        }

        wp_send_json_success(array(
            'prefix'  => $prefix,
            'carrier' => $carrier,
            'count'   => count($out),
            'capped'  => count($out) >= 250,
            'rows'    => $out,
        ));
    }

    
    public function ajax_metrics_export_aggregate() {
        $this->verify_admin_ajax();

        if (!class_exists('AIEO_DMM_XLSX_Writer')) {
            require_once AIEO_DIR . 'includes/dmm/services/class-aieo-dmm-xlsx-writer.php';
        }

        $carrier = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : '';
        $prefix_len = (int) AIEO_Fulfillment_Settings::get('metrics_postcode_prefix_length', 2);
        if ($prefix_len < 1 || $prefix_len > 5) { $prefix_len = 2; }

        global $wpdb;
        $vt = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $ot = $wpdb->prefix . 'wc_orders';
        $at = $wpdb->prefix . 'wc_order_addresses';

        $where_carrier = '';
        $params = array();
        if ($carrier !== '') {
            $where_carrier = 'AND v.carrier = %s';
            $params[] = $carrier;
        }

        $select_metrics = "
                COUNT(*) AS shipments,
                AVG(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.created_at))   AS avg_h_o2v,
                MIN(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.created_at))   AS min_h_o2v,
                MAX(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.created_at))   AS max_h_o2v,
                AVG(TIMESTAMPDIFF(HOUR, v.created_at, v.delivered_at))       AS avg_h_v2d,
                MIN(TIMESTAMPDIFF(HOUR, v.created_at, v.delivered_at))       AS min_h_v2d,
                MAX(TIMESTAMPDIFF(HOUR, v.created_at, v.delivered_at))       AS max_h_v2d,
                AVG(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at)) AS avg_h_o2d,
                MIN(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at)) AS min_h_o2d,
                MAX(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at)) AS max_h_o2d,
                AVG(v.customer_shipping_paid)                                AS avg_customer_paid,
                AVG(v.carrier_cost)                                          AS avg_carrier_cost,
                AVG(CASE WHEN v.carrier_cost IS NOT NULL
                         THEN v.customer_shipping_paid - v.carrier_cost
                         ELSE NULL END)                                      AS avg_net,
                AVG(GREATEST(o.total_amount - COALESCE(v.customer_shipping_paid, 0), 0)) AS avg_net_products
        ";
        $margin = AIEO_DMM::get_gross_margin();
        $sanity_where = self::delivery_metrics_sanity_where();
        $from_join = "
            FROM {$vt} v
            INNER JOIN {$ot} o ON o.id = v.order_id
            INNER JOIN {$at} a ON a.order_id = v.order_id AND a.address_type = 'shipping'
            WHERE v.delivered_at IS NOT NULL
              AND v.created_at   IS NOT NULL
              AND a.postcode     IS NOT NULL
              AND a.postcode     <> ''
              {$sanity_where}
              {$where_carrier}
        ";

        
        $sql = "
            SELECT LEFT(REPLACE(CAST(a.postcode AS CHAR), ' ', ''), {$prefix_len}) AS prefix,
                   {$select_metrics}
            {$from_join}
            GROUP BY prefix
            HAVING shipments > 0
            ORDER BY shipments DESC, prefix ASC
        ";
        $rows = empty($params) ? $wpdb->get_results($sql, ARRAY_A) : $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);

        $totals_sql = "SELECT {$select_metrics} {$from_join}";
        $totals = empty($params) ? $wpdb->get_row($totals_sql, ARRAY_A) : $wpdb->get_row($wpdb->prepare($totals_sql, $params), ARRAY_A);

        
        
        $h = static function ($v) { return $v === null ? '' : round((float) $v, 1); };
        $m = static function ($v) { return $v === null ? '' : round((float) $v, 2); };

        $couriers = $this->couriers();
        $carrier_label = ($carrier !== '' && isset($couriers[$carrier])) ? $couriers[$carrier]->label() : __('all carriers', 'ai-eshop-optimizer');

        $w = new AIEO_DMM_XLSX_Writer(sprintf('Postal-area metrics — %s', $carrier_label));
        $w->add_header_row(array(
            __('Postal prefix', 'ai-eshop-optimizer'),
            __('Shipments', 'ai-eshop-optimizer'),
            __('Order → voucher AVG (h)', 'ai-eshop-optimizer'),
            __('Order → voucher MIN (h)', 'ai-eshop-optimizer'),
            __('Order → voucher MAX (h)', 'ai-eshop-optimizer'),
            __('Voucher → delivery AVG (h)', 'ai-eshop-optimizer'),
            __('Voucher → delivery MIN (h)', 'ai-eshop-optimizer'),
            __('Voucher → delivery MAX (h)', 'ai-eshop-optimizer'),
            __('Order → delivery AVG (h)', 'ai-eshop-optimizer'),
            __('Order → delivery MIN (h)', 'ai-eshop-optimizer'),
            __('Order → delivery MAX (h)', 'ai-eshop-optimizer'),
            __('Avg paid (€)', 'ai-eshop-optimizer'),
            __('Avg cost (€)', 'ai-eshop-optimizer'),
            __('Avg net (€)', 'ai-eshop-optimizer'),
            __('Avg profit (€)', 'ai-eshop-optimizer'),
            __('Min order (€)', 'ai-eshop-optimizer'),
            __('Free-ship threshold (€)', 'ai-eshop-optimizer'),
        ));
        $fs_threshold = AIEO_DMM::get_free_shipping_threshold();
        
        
        $col_formats = array(
            0  => 'string',
            1  => 'integer',
            2  => 'number', 3  => 'number', 4  => 'number',
            5  => 'number', 6  => 'number', 7  => 'number',
            8  => 'number', 9  => 'number', 10 => 'number',
            11 => 'currency', 12 => 'currency', 13 => 'currency', 14 => 'currency', 15 => 'currency', 16 => 'currency',
        );
        
        
        $hi = static function ($v) { return $v === null || $v === '' ? '' : (int) round((float) $v); };
        foreach ((array) $rows as $r) {
            $w->add_row(array(
                (string) $r['prefix'],
                (int)    $r['shipments'],
                $h($r['avg_h_o2v']), $hi($r['min_h_o2v']), $hi($r['max_h_o2v']),
                $h($r['avg_h_v2d']), $hi($r['min_h_v2d']), $hi($r['max_h_v2d']),
                $h($r['avg_h_o2d']), $hi($r['min_h_o2d']), $hi($r['max_h_o2d']),
                $m($r['avg_customer_paid']),
                $m($r['avg_carrier_cost']),
                $m($r['avg_net']),
                $m($r['avg_net_products'] !== null ? (float) $r['avg_net_products'] * $margin : null),
                $m(($r['avg_carrier_cost'] !== null && $margin > 0) ? (float) $r['avg_carrier_cost'] / $margin : null),
                $m(($r['avg_carrier_cost'] !== null && $margin > 0) ? $fs_threshold + (float) $r['avg_carrier_cost'] / $margin : null),
            ), $col_formats);
        }

        
        if (is_array($totals)) {
            $w->add_row(array(
                __('TOTAL', 'ai-eshop-optimizer'),
                (int) $totals['shipments'],
                $h($totals['avg_h_o2v']), $hi($totals['min_h_o2v']), $hi($totals['max_h_o2v']),
                $h($totals['avg_h_v2d']), $hi($totals['min_h_v2d']), $hi($totals['max_h_v2d']),
                $h($totals['avg_h_o2d']), $hi($totals['min_h_o2d']), $hi($totals['max_h_o2d']),
                $m($totals['avg_customer_paid']),
                $m($totals['avg_carrier_cost']),
                $m($totals['avg_net']),
                $m($totals['avg_net_products'] !== null ? (float) $totals['avg_net_products'] * $margin : null),
                $m(($totals['avg_carrier_cost'] !== null && $margin > 0) ? (float) $totals['avg_carrier_cost'] / $margin : null),
                $m(($totals['avg_carrier_cost'] !== null && $margin > 0) ? $fs_threshold + (float) $totals['avg_carrier_cost'] / $margin : null),
            ), $col_formats);
        }

        $filename = sprintf(
            'metrics-postal-areas-%s-%s.xlsx',
            ($carrier !== '' ? $carrier : 'all'),
            gmdate('Ymd-His')
        );
        $w->stream_download($filename); 
    }

    
    public function ajax_metrics_export() {
        $this->verify_admin_ajax();

        if (!class_exists('AIEO_DMM_XLSX_Writer')) {
            require_once AIEO_DIR . 'includes/dmm/services/class-aieo-dmm-xlsx-writer.php';
        }

        $prefix     = isset($_POST['prefix']) ? (string) wp_unslash($_POST['prefix']) : '';
        $prefix_len = isset($_POST['prefix_len']) ? (int) $_POST['prefix_len'] : 2;
        if ($prefix_len < 1 || $prefix_len > 5) { $prefix_len = 2; }
        $prefix = preg_replace('/[^0-9A-Za-z]/', '', $prefix);
        if ($prefix !== '' && strlen($prefix) > $prefix_len) {
            wp_die(esc_html__('invalid prefix', 'ai-eshop-optimizer'));
        }
        $carrier = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : '';
        $bucket  = isset($_POST['bucket'])  ? sanitize_key((string) wp_unslash($_POST['bucket']))  : '';
        if ($prefix === '' && $bucket === '') {
            wp_die(esc_html__('either prefix or bucket required', 'ai-eshop-optimizer'));
        }

        global $wpdb;
        $vt = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $ot = $wpdb->prefix . 'wc_orders';
        $at = $wpdb->prefix . 'wc_order_addresses';

        $where_clauses = array();
        $params        = array();
        if ($prefix !== '') {
            $where_clauses[] = "LEFT(REPLACE(CAST(a.postcode AS CHAR), ' ', ''), {$prefix_len}) = %s";
            $params[]        = $prefix;
        }
        $bucket_sql = self::bucket_where_sql($bucket);
        if ($bucket_sql !== '') {
            $where_clauses[] = $bucket_sql;
        }
        $where_carrier = '';
        if ($carrier !== '') {
            $where_carrier = 'AND v.carrier = %s';
            $params[]      = $carrier;
        }

        $where_extra = empty($where_clauses) ? '' : ' AND (' . implode(') AND (', $where_clauses) . ')';
        $sanity_where = self::delivery_metrics_sanity_where();
        
        $sql = "
            SELECT
                v.order_id, v.carrier, v.voucher_no, v.delivered_at,
                a.first_name, a.last_name, a.address_1, a.address_2, a.city, a.postcode, a.country,
                o.total_amount,
                TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at) AS h_order_to_delivery
            FROM {$vt} v
            INNER JOIN {$ot} o ON o.id = v.order_id
            INNER JOIN {$at} a ON a.order_id = v.order_id AND a.address_type = 'shipping'
            WHERE v.delivered_at IS NOT NULL
              AND a.postcode IS NOT NULL AND a.postcode <> ''
              {$sanity_where}
              {$where_extra}
              {$where_carrier}
            ORDER BY v.delivered_at DESC, v.id DESC
        ";
        $rows = empty($params) ? $wpdb->get_results($sql, ARRAY_A) : $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);

        $couriers       = $this->couriers();
        $carrier_labels = array();
        foreach ($couriers as $code => $driver) {
            $carrier_labels[$code] = $driver->label();
        }

        $sheet_name = sprintf('Postal %s', $prefix);
        if ($carrier !== '' && isset($carrier_labels[$carrier])) {
            
            
            $sheet_name = sprintf('%s — %s', $sheet_name, $carrier_labels[$carrier]);
        }

        $w = new AIEO_DMM_XLSX_Writer($sheet_name);
        $w->add_header_row(array(
            __('Order ID', 'ai-eshop-optimizer'),
            __('Carrier', 'ai-eshop-optimizer'),
            __('Voucher', 'ai-eshop-optimizer'),
            __('Delivered at', 'ai-eshop-optimizer'),
            __('Customer', 'ai-eshop-optimizer'),
            __('Address 1', 'ai-eshop-optimizer'),
            __('Address 2', 'ai-eshop-optimizer'),
            __('City', 'ai-eshop-optimizer'),
            __('Postcode', 'ai-eshop-optimizer'),
            __('Country', 'ai-eshop-optimizer'),
            __('Order → delivery (h)', 'ai-eshop-optimizer'),
            __('Order → delivery (d)', 'ai-eshop-optimizer'),
            __('Order value (€)', 'ai-eshop-optimizer'),
        ));

        $col_formats = array(0 => 'integer', 10 => 'integer', 11 => 'number', 12 => 'currency');
        foreach ((array) $rows as $r) {
            $carrier_code = (string) $r['carrier'];
            $name = trim(((string) $r['first_name']) . ' ' . ((string) $r['last_name']));
            $h = $r['h_order_to_delivery'];
            $hours = ($h === null || $h === '') ? '' : (int) round((float) $h);
            $days  = ($h === null || $h === '') ? '' : round(((float) $h) / 24, 1);
            $w->add_row(array(
                (int) $r['order_id'],
                isset($carrier_labels[$carrier_code]) ? $carrier_labels[$carrier_code] : strtoupper($carrier_code),
                (string) $r['voucher_no'],
                (string) $r['delivered_at'],
                $name,
                (string) $r['address_1'],
                (string) $r['address_2'],
                (string) $r['city'],
                (string) $r['postcode'],
                (string) $r['country'],
                $hours,
                $days,
                (float) $r['total_amount'],
            ), $col_formats);
        }

        $scope = ($prefix !== '' ? 'p' . $prefix : '');
        if ($bucket !== '') {
            $scope = ($scope === '' ? '' : $scope . '-') . 'b' . $bucket;
        }
        $filename = sprintf(
            'shipments-%s-%s-%s.xlsx',
            ($carrier !== '' ? $carrier : 'all'),
            $scope,
            gmdate('Ymd-His')
        );
        $w->stream_download($filename); 
    }

    
    public function ajax_backfill_detailed_batch() {
        $this->verify_admin_ajax();

        $carrier = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : 'geniki';
        $offset  = isset($_POST['offset'])  ? max(0, (int) $_POST['offset']) : 0;
        $batch   = isset($_POST['batch'])   ? max(1, min(50, (int) $_POST['batch'])) : 10;

        $couriers = $this->couriers();
        if (!isset($couriers[$carrier])) {
            wp_send_json_error(array('message' => 'unknown_carrier'));
        }
        $driver = $couriers[$carrier];
        if (!$driver->has_capability('track')) {
            wp_send_json_error(array('message' => 'driver_no_track_capability'));
        }

        $consec_in  = isset($_POST['consecutive_errors']) ? max(0, (int) $_POST['consecutive_errors']) : 0;
        $consec_max = 5;
        $consec     = $consec_in;
        $aborted    = false;
        $sweep_mode = !empty($_POST['sweep']);

        $terminal_not_found_codes = array(9);

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        
        
        
        
        
        $total = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table}
             WHERE carrier = %s
               AND delivered_at IS NOT NULL
               AND arrived_at   IS NULL
               AND status NOT IN ('canceled','returned','untrackable')",
            $carrier
        ));

        
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, voucher_no FROM {$table}
             WHERE carrier = %s
               AND delivered_at IS NOT NULL
               AND arrived_at   IS NULL
               AND status NOT IN ('canceled','returned','untrackable')
             ORDER BY id DESC
             LIMIT %d",
            $carrier,
            $batch
        ), ARRAY_A);

        $populated_arrived = 0;
        $no_c_a3           = 0; 
        $marked_untrackable = 0;
        $errors            = array();

        foreach ((array) $rows as $row) {
            if (!$sweep_mode && $consec >= $consec_max) { $aborted = true; break; }

            $vid = (int) $row['id'];
            $vno = (string) $row['voucher_no'];
            if ($vno === '') { continue; }

            $r = $driver->track($vno); 
            if (empty($r['ok'])) {
                $code_int   = isset($r['code']) ? (int) $r['code'] : 0;
                $is_terminal = in_array($code_int, $terminal_not_found_codes, true);
                if ($is_terminal) {
                    
                    
                    
                    
                    
                    AIEO_Fulfillment_Vouchers::update($vid, array(
                        'status'         => 'untrackable',
                        'status_code'    => 'CARRIER_NOT_FOUND_' . $code_int,
                        'status_at'      => current_time('mysql', true),
                        'last_polled_at' => current_time('mysql', true),
                    ));
                    $marked_untrackable++;
                } else {
                    $errors[] = $vno . ': ' . (isset($r['error']) ? (string) $r['error'] : 'track_failed');
                    AIEO_Fulfillment_Vouchers::update($vid, array(
                        'last_polled_at' => current_time('mysql', true),
                    ));
                }
                $consec++;
                if (!$sweep_mode && $consec >= $consec_max) { $aborted = true; break; }
                continue;
            }
            $consec = 0;

            
            
            
            if (!empty($r['checkpoints'])) {
                AIEO_Fulfillment_Vouchers::record_checkpoints($vid, $r['checkpoints']);
            }

            $patch = array(
                'last_polled_at' => current_time('mysql', true),
            );
            if (!empty($r['arrived_at'])) {
                $patch['arrived_at']     = gmdate('Y-m-d H:i:s', (int) $r['arrived_at']);
                $patch['arrived_status'] = isset($r['arrived_status']) ? sanitize_key((string) $r['arrived_status']) : 'C_A3';
                $populated_arrived++;
            } else {
                $no_c_a3++;
            }
            
            
            if (!empty($r['delivered_at'])) {
                $patch['delivered_at'] = gmdate('Y-m-d H:i:s', (int) $r['delivered_at']);
            }
            if (!empty($r['consignee'])) {
                $patch['consignee'] = sanitize_text_field((string) $r['consignee']);
            }
            AIEO_Fulfillment_Vouchers::update($vid, $patch);
        }

        $processed_this_batch = is_array($rows) ? count($rows) : 0;
        $complete = ($processed_this_batch === 0) || $aborted;

        wp_send_json_success(array(
            'processed'           => $processed_this_batch,
            'arrived_populated'   => $populated_arrived,
            'no_c_a3'             => $no_c_a3,
            'untrackable'         => $marked_untrackable,
            'errors'              => array_slice($errors, 0, 10),
            'remaining'           => max(0, $total - $processed_this_batch),
            'complete'            => $complete,
            'consecutive_errors'  => $consec,
            'aborted_consecutive' => $aborted,
        ));
    }

    
    public function backfill_customer_shipping_paid($batch = 500) {
        global $wpdb;
        if (!function_exists('wc_get_order')) {
            return array('processed' => 0, 'updated' => 0, 'remaining' => 0, 'error' => 'wc_get_order unavailable');
        }
        $table  = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $batch  = max(1, min(2000, (int) $batch));

        
        
        $total = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$table} WHERE customer_shipping_paid IS NULL AND order_id > 0"
        );

        
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, order_id FROM {$table}
             WHERE customer_shipping_paid IS NULL AND order_id > 0
             ORDER BY id DESC
             LIMIT %d",
            $batch
        ), ARRAY_A);

        $updated = 0;
        foreach ((array) $rows as $row) {
            $order = wc_get_order((int) $row['order_id']);
            if (!$order instanceof WC_Order) { continue; }
            $paid = (float) $order->get_shipping_total();
            AIEO_Fulfillment_Vouchers::update((int) $row['id'], array(
                'customer_shipping_paid' => $paid,
            ));
            $updated++;
        }
        return array(
            'processed' => count((array) $rows),
            'updated'   => $updated,
            'remaining' => max(0, $total - count((array) $rows)),
        );
    }

    
    public function ajax_backfill_customer_paid_batch() {
        $this->verify_admin_ajax();
        $batch  = isset($_POST['batch']) ? max(1, min(2000, (int) $_POST['batch'])) : 500;
        $result = $this->backfill_customer_shipping_paid($batch);
        $result['complete'] = ($result['remaining'] === 0);
        wp_send_json_success($result);
    }

    
    public function ajax_backfill_costs_batch() {
        $this->verify_admin_ajax();

        $carrier = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : 'geniki';
        $batch   = isset($_POST['batch'])   ? max(1, min(50, (int) $_POST['batch'])) : 10;

        $couriers = $this->couriers();
        if (!isset($couriers[$carrier])) {
            wp_send_json_error(array('message' => 'unknown_carrier'));
        }
        $driver = $couriers[$carrier];
        if (!$driver->has_capability('calculate_cost')) {
            wp_send_json_error(array('message' => 'driver_no_calculate_cost_capability'));
        }

        $cost_disabled_codes = array(12); 

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        
        
        
        
        
        $total = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table}
             WHERE carrier = %s
               AND carrier_cost IS NULL
               AND status NOT IN ('canceled','returned','untrackable')",
            $carrier
        ));

        
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, voucher_no, weight, services_csv FROM {$table}
             WHERE carrier = %s
               AND carrier_cost IS NULL
               AND status NOT IN ('canceled','returned','untrackable')
             ORDER BY id DESC
             LIMIT %d",
            $carrier,
            $batch
        ), ARRAY_A);

        $costed = 0;
        $errors = array();
        $account_disabled = false;

        foreach ((array) $rows as $row) {
            $vid = (int) $row['id'];
            $vno = (string) $row['voucher_no'];
            if ($vno === '') { continue; }

            $weight = isset($row['weight']) ? (float) $row['weight'] : 0;
            if ($weight <= 0) { $weight = 0.5; }

            $cost_r = $driver->calculate_cost(array(
                'weight'       => $weight,
                'services_csv' => (string) ($row['services_csv'] ?? ''),
                'recipient'    => array('country' => 'Greece'),
            ));

            if (!empty($cost_r['ok'])) {
                AIEO_Fulfillment_Vouchers::update($vid, array(
                    'carrier_cost' => isset($cost_r['cost']) ? (float) $cost_r['cost'] : null,
                    'carrier_vat'  => isset($cost_r['vat'])  ? (float) $cost_r['vat']  : null,
                    'cost_source'  => 'estimate_current_tariff',
                ));
                $costed++;
                continue;
            }

            $code_int = isset($cost_r['code']) ? (int) $cost_r['code'] : 0;
            if (in_array($code_int, $cost_disabled_codes, true)) {
                
                
                
                $account_disabled = true;
                $errors[] = sprintf(
                    'CalculateCost returned code %d — likely not enabled on this account. Run halted; ask Geniki to enable it, then re-run.',
                    $code_int
                );
                break;
            }
            $errors[] = $vno . ': ' . (isset($cost_r['error']) ? (string) $cost_r['error'] : 'cost_failed');
        }

        $processed_this_batch = is_array($rows) ? count($rows) : 0;
        $complete = ($processed_this_batch === 0) || $account_disabled;

        wp_send_json_success(array(
            'processed'        => $processed_this_batch,
            'costed'           => $costed,
            'errors'           => array_slice($errors, 0, 10),
            'remaining'        => max(0, $total - $processed_this_batch),
            'complete'         => $complete,
            'account_disabled' => $account_disabled,
        ));
    }

    
    public function ajax_import_geniki_invoice_csv() {
        $this->verify_admin_ajax();

        if (empty($_FILES['file']) || !is_array($_FILES['file'])) {
            wp_send_json_error(array('message' => __('No file uploaded.', 'ai-eshop-optimizer')));
        }
        $f = $_FILES['file'];
        if ((int) ($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
            wp_send_json_error(array('message' => sprintf(
                
                __('Upload error %d. Check upload_max_filesize / post_max_size.', 'ai-eshop-optimizer'),
                (int) ($f['error'] ?? 0)
            )));
        }
        $tmp = (string) ($f['tmp_name'] ?? '');
        if ($tmp === '' || !is_uploaded_file($tmp)) {
            wp_send_json_error(array('message' => __('Invalid upload.', 'ai-eshop-optimizer')));
        }

        $raw = (string) file_get_contents($tmp);
        if ($raw === '') {
            wp_send_json_error(array('message' => __('Empty file.', 'ai-eshop-optimizer')));
        }

        
        
        
        
        
        $raw = preg_replace('/^\xEF\xBB\xBF/', '', $raw);
        if (function_exists('mb_check_encoding') && !mb_check_encoding($raw, 'UTF-8')) {
            if (function_exists('iconv')) {
                $converted = @iconv('WINDOWS-1253', 'UTF-8//TRANSLIT', $raw);
                if ($converted !== false) { $raw = $converted; }
            } elseif (function_exists('mb_convert_encoding')) {
                $raw = mb_convert_encoding($raw, 'UTF-8', 'Windows-1253');
            }
        }

        
        $raw = str_replace(array("\r\n", "\r"), "\n", $raw);

        
        
        $lines = explode("\n", $raw);
        if (empty($lines)) {
            wp_send_json_error(array('message' => __('No rows parsed.', 'ai-eshop-optimizer')));
        }

        $header_seen = false;
        
        $rows_by_voucher = array();
        $row_errors = array();
        $parsed_rows = 0;

        foreach ($lines as $idx => $line) {
            if ($line === '') { continue; }
            $cols = str_getcsv($line, ';');
            if (count($cols) < 25) { continue; } 

            
            $voucher = trim((string) ($cols[2] ?? ''));
            if (!$header_seen && !ctype_digit($voucher)) {
                $header_seen = true;
                continue;
            }
            if (!ctype_digit($voucher) || strlen($voucher) < 6) {
                
                continue;
            }
            $parsed_rows++;

            
            
            $total_raw = trim((string) ($cols[24] ?? ''));
            $total_raw = str_replace(array('.', ' '), '', $total_raw); 
            $total_raw = str_replace(',', '.', $total_raw);
            if (!is_numeric($total_raw)) {
                $row_errors[] = sprintf('row %d voucher=%s: cost not numeric (%s)',
                    $idx + 1, $voucher, (string) ($cols[24] ?? ''));
                continue;
            }
            $cost = (float) $total_raw;
            if ($cost < 0) {
                $row_errors[] = sprintf('row %d voucher=%s: negative cost (%.2f)',
                    $idx + 1, $voucher, $cost);
                continue;
            }

            
            $weight_raw = trim((string) ($cols[12] ?? ''));
            $weight_raw = str_replace(',', '.', str_replace(' ', '', $weight_raw));
            $weight = is_numeric($weight_raw) ? (float) $weight_raw : null;

            
            
            $rows_by_voucher[$voucher] = array(
                'cost'        => $cost,
                'pieces'      => max(1, (int) trim((string) ($cols[11] ?? '1'))),
                'weight'      => $weight,
                'consignee'   => mb_substr(trim((string) ($cols[10] ?? '')), 0, 120),
                'invoice_no'  => mb_substr(trim((string) ($cols[1]  ?? '')), 0,  80),
                'pickup_date' => mb_substr(trim((string) ($cols[3]  ?? '')), 0,  80),
            );
        }

        if (empty($rows_by_voucher)) {
            wp_send_json_error(array(
                'message'      => __('No valid voucher rows found in file.', 'ai-eshop-optimizer'),
                'parsed_rows'  => $parsed_rows,
                'errors'       => array_slice($row_errors, 0, 10),
            ));
        }

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        
        
        
        
        $existing_set = array();
        foreach (array_chunk(array_keys($rows_by_voucher), 500) as $chunk) {
            $ph = implode(',', array_fill(0, count($chunk), '%s'));
            $found = $wpdb->get_col($wpdb->prepare(
                "SELECT voucher_no FROM {$table} WHERE carrier='geniki' AND voucher_no IN ({$ph})",
                $chunk
            ));
            foreach ((array) $found as $v) { $existing_set[$v] = true; }
        }
        $matched_count = count($existing_set);

        
        
        
        
        $updated_count = 0;
        $matched_keys = array_keys($existing_set);
        foreach (array_chunk($matched_keys, 500) as $chunk) {
            $placeholders = implode(',', array_fill(0, count($chunk), '%s'));
            $when_sql = '';
            $params = array();
            foreach ($chunk as $v) {
                $when_sql .= ' WHEN voucher_no = %s THEN %f';
                $params[] = $v;
                $params[] = $rows_by_voucher[$v]['cost'];
            }
            $update_sql = "UPDATE {$table}
                              SET carrier_cost = CASE {$when_sql} END,
                                  cost_source  = 'invoice_csv'
                            WHERE carrier='geniki' AND voucher_no IN ({$placeholders})";
            $params = array_merge($params, $chunk);
            $r = $wpdb->query($wpdb->prepare($update_sql, $params));
            if ($r !== false) { $updated_count += (int) $r; }
        }

        
        
        
        
        
        
        
        $orphan_keys = array_diff_key($rows_by_voucher, $existing_set);
        $orphans_inserted = 0;
        $orphans_updated  = 0;
        foreach (array_chunk($orphan_keys, 200, true) as $chunk) {
            $values_sql = array();
            $params = array();
            foreach ($chunk as $voucher_no => $r) {
                $values_sql[] = '(0, %s, %s, %d, ' . ($r['weight'] !== null ? '%f' : 'NULL') . ', %f, %s, %s, %s, %s, %s)';
                $params[] = 'geniki';                  
                $params[] = (string) $voucher_no;
                $params[] = (int)    $r['pieces'];
                if ($r['weight'] !== null) { $params[] = (float) $r['weight']; }
                $params[] = (float) $r['cost'];
                $params[] = 'invoice_csv';             
                $params[] = 'orphan_invoice';          
                $params[] = (string) $r['consignee'];
                $params[] = (string) $r['invoice_no'];  
                $params[] = (string) $r['pickup_date']; 
            }
            $sql = "INSERT INTO {$table}
                    (order_id, carrier, voucher_no, pieces, weight, carrier_cost,
                     cost_source, status, consignee, reference_1, reference_2)
                    VALUES " . implode(',', $values_sql) . "
                    ON DUPLICATE KEY UPDATE
                        carrier_cost = VALUES(carrier_cost),
                        cost_source  = VALUES(cost_source),
                        consignee    = VALUES(consignee),
                        reference_1  = VALUES(reference_1),
                        reference_2  = VALUES(reference_2)";
            $r = $wpdb->query($wpdb->prepare($sql, $params));
            if ($r !== false) {
                
                
                
                
                
                
                $orphans_inserted += count($chunk);
            }
        }
        $orphan_count = count($orphan_keys);

        wp_send_json_success(array(
            'parsed_rows'     => $parsed_rows,
            'unique_vouchers' => count($rows_by_voucher),
            'matched'         => $matched_count,
            'updated'         => $updated_count,
            'unmatched'       => $orphan_count,
            'orphans_stored'  => $orphans_inserted, 
            'errors'          => array_slice($row_errors, 0, 10),
        ));
    }

    
    public function ajax_import_carrier_invoice_csv() {
        $this->verify_admin_ajax();
        $carrier = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : '';
        if ($carrier === '') {
            wp_send_json_error(array('message' => __('Pick a carrier.', 'ai-eshop-optimizer')));
        }
        $map = $this->invoice_csv_map($carrier);
        $override = array();
        foreach (array('delimiter', 'encoding', 'decimal') as $k) {
            if (isset($_POST[$k]) && $_POST[$k] !== '') { $override[$k] = sanitize_text_field((string) wp_unslash($_POST[$k])); }
        }
        foreach (array('min_cols', 'voucher_col', 'cost_col', 'weight_col', 'pieces_col', 'consignee_col', 'invoice_col', 'date_col', 'voucher_min_len', 'voucher_digits_only') as $k) {
            if (isset($_POST[$k]) && $_POST[$k] !== '') { $override[$k] = (int) $_POST[$k]; }
        }
        if (!empty($override)) {
            $map = array_merge($map, $override);
            if (class_exists('AIEO_Fulfillment_Settings')) {
                AIEO_Fulfillment_Settings::set('invoice_csv_map_' . $carrier, $map);
            }
        }
        $raw = $this->read_uploaded_csv();
        $res = $this->import_invoice_csv($carrier, $raw, $map);
        if (empty($res['ok'])) { wp_send_json_error($res); }
        unset($res['ok']);
        wp_send_json_success($res);
    }

    
    private function read_uploaded_csv() {
        if (empty($_FILES['file']) || !is_array($_FILES['file'])) {
            wp_send_json_error(array('message' => __('No file uploaded.', 'ai-eshop-optimizer')));
        }
        $f = $_FILES['file'];
        if ((int) ($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
            wp_send_json_error(array('message' => sprintf(
                
                __('Upload error %d. Check upload_max_filesize / post_max_size.', 'ai-eshop-optimizer'),
                (int) ($f['error'] ?? 0)
            )));
        }
        $tmp = (string) ($f['tmp_name'] ?? '');
        if ($tmp === '' || !is_uploaded_file($tmp)) {
            wp_send_json_error(array('message' => __('Invalid upload.', 'ai-eshop-optimizer')));
        }
        $raw = (string) file_get_contents($tmp);
        if ($raw === '') {
            wp_send_json_error(array('message' => __('Empty file.', 'ai-eshop-optimizer')));
        }
        return $raw;
    }

    
    private function invoice_csv_map($carrier) {
        $carrier = sanitize_key((string) $carrier);
        $builtin = array(
            'geniki' => array(
                'delimiter' => ';', 'encoding' => 'Windows-1253', 'decimal' => 'comma',
                'min_cols' => 25, 'voucher_col' => 2, 'cost_col' => 24, 'weight_col' => 12,
                'pieces_col' => 11, 'consignee_col' => 10, 'invoice_col' => 1, 'date_col' => 3,
                'voucher_min_len' => 6, 'voucher_digits_only' => 1,
            ),
        );
        $default = isset($builtin[$carrier]) ? $builtin[$carrier] : array(
            'delimiter' => ',', 'encoding' => 'UTF-8', 'decimal' => 'dot',
            'min_cols' => 2, 'voucher_col' => 0, 'cost_col' => 1, 'weight_col' => -1,
            'pieces_col' => -1, 'consignee_col' => -1, 'invoice_col' => -1, 'date_col' => -1,
            'voucher_min_len' => 4, 'voucher_digits_only' => 0,
        );
        $stored = class_exists('AIEO_Fulfillment_Settings')
            ? (array) AIEO_Fulfillment_Settings::get('invoice_csv_map_' . $carrier, array())
            : array();
        return array_merge($default, $stored);
    }

    
    private function import_invoice_csv($carrier, $raw, array $map) {
        $carrier  = sanitize_key((string) $carrier);
        $delim    = (string) ($map['delimiter'] ?? ';');
        if (strtolower($delim) === 'tab' || $delim === '\\t') { $delim = "\t"; }
        $delim    = ($delim === '') ? ';' : substr($delim, 0, 1);
        $enc      = (string) ($map['encoding'] ?? 'UTF-8');
        $decimal  = (string) ($map['decimal'] ?? 'dot');
        $min_cols = max(1, (int) ($map['min_cols'] ?? 1));
        $vc       = (int) ($map['voucher_col'] ?? 0);
        $costc    = (int) ($map['cost_col'] ?? 1);
        $wcix     = (int) ($map['weight_col'] ?? -1);
        $pcix     = (int) ($map['pieces_col'] ?? -1);
        $ncix     = (int) ($map['consignee_col'] ?? -1);
        $icix     = (int) ($map['invoice_col'] ?? -1);
        $dcix     = (int) ($map['date_col'] ?? -1);
        $vmin     = max(1, (int) ($map['voucher_min_len'] ?? 4));
        $vdigits  = !empty($map['voucher_digits_only']);

        
        
        $raw = preg_replace('/^\xEF\xBB\xBF/', '', (string) $raw);
        if (strtoupper($enc) !== 'UTF-8' && function_exists('mb_check_encoding') && !mb_check_encoding($raw, 'UTF-8')) {
            if (function_exists('iconv')) {
                $conv = @iconv($enc, 'UTF-8//TRANSLIT', $raw);
                if ($conv !== false) { $raw = $conv; }
            } elseif (function_exists('mb_convert_encoding')) {
                $raw = mb_convert_encoding($raw, 'UTF-8', $enc);
            }
        }
        $raw   = str_replace(array("\r\n", "\r"), "\n", $raw);
        $lines = explode("\n", $raw);

        $to_num = static function ($s) use ($decimal) {
            $s = trim((string) $s);
            if ($s === '') { return ''; }
            if ($decimal === 'comma') {
                $s = str_replace(array('.', ' '), '', $s);
                $s = str_replace(',', '.', $s);
            } else {
                $s = str_replace(array(',', ' '), '', $s);
            }
            return $s;
        };

        $header_seen     = false;
        $rows_by_voucher = array();
        $row_errors      = array();
        $parsed_rows     = 0;

        foreach ($lines as $idx => $line) {
            if ($line === '') { continue; }
            $cols = str_getcsv($line, $delim);
            if (count($cols) < $min_cols) { continue; }

            $voucher    = trim((string) ($cols[$vc] ?? ''));
            $is_voucher = $vdigits
                ? (ctype_digit($voucher) && strlen($voucher) >= $vmin)
                : ($voucher !== '' && strlen($voucher) >= $vmin);
            if (!$header_seen && !$is_voucher) { $header_seen = true; continue; }
            if (!$is_voucher) { continue; }
            $parsed_rows++;

            $total_raw = $to_num($cols[$costc] ?? '');
            if (!is_numeric($total_raw)) {
                $row_errors[] = sprintf('row %d voucher=%s: cost not numeric (%s)', $idx + 1, $voucher, (string) ($cols[$costc] ?? ''));
                continue;
            }
            $cost = (float) $total_raw;
            if ($cost < 0) {
                $row_errors[] = sprintf('row %d voucher=%s: negative cost (%.2f)', $idx + 1, $voucher, $cost);
                continue;
            }

            $weight = null;
            if ($wcix >= 0) {
                $w = $to_num($cols[$wcix] ?? '');
                if (is_numeric($w)) { $weight = (float) $w; }
            }

            $rows_by_voucher[$voucher] = array(
                'cost'        => $cost,
                'pieces'      => $pcix >= 0 ? max(1, (int) trim((string) ($cols[$pcix] ?? '1'))) : 1,
                'weight'      => $weight,
                'consignee'   => $ncix >= 0 ? mb_substr(trim((string) ($cols[$ncix] ?? '')), 0, 120) : '',
                'invoice_no'  => $icix >= 0 ? mb_substr(trim((string) ($cols[$icix] ?? '')), 0,  80) : '',
                'pickup_date' => $dcix >= 0 ? mb_substr(trim((string) ($cols[$dcix] ?? '')), 0,  80) : '',
            );
        }

        if (empty($rows_by_voucher)) {
            return array(
                'ok'          => false,
                'message'     => __('No valid voucher rows found in file. Check the column mapping (delimiter / voucher column / cost column).', 'ai-eshop-optimizer'),
                'parsed_rows' => $parsed_rows,
                'errors'      => array_slice($row_errors, 0, 10),
            );
        }

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        
        $existing_set = array();
        foreach (array_chunk(array_keys($rows_by_voucher), 500) as $chunk) {
            $ph   = implode(',', array_fill(0, count($chunk), '%s'));
            $args = array_merge(array($carrier), $chunk);
            $found = $wpdb->get_col($wpdb->prepare(
                "SELECT voucher_no FROM {$table} WHERE carrier=%s AND voucher_no IN ({$ph})",
                $args
            ));
            foreach ((array) $found as $v) { $existing_set[$v] = true; }
        }
        $matched_count = count($existing_set);

        
        $updated_count = 0;
        foreach (array_chunk(array_keys($existing_set), 500) as $chunk) {
            $placeholders = implode(',', array_fill(0, count($chunk), '%s'));
            $when_sql = '';
            $params   = array();
            foreach ($chunk as $v) {
                $when_sql .= ' WHEN voucher_no = %s THEN %f';
                $params[]  = $v;
                $params[]  = $rows_by_voucher[$v]['cost'];
            }
            $update_sql = "UPDATE {$table}
                              SET carrier_cost = CASE {$when_sql} END,
                                  cost_source  = 'invoice_csv'
                            WHERE carrier=%s AND voucher_no IN ({$placeholders})";
            $params[] = $carrier;
            $params   = array_merge($params, $chunk);
            $r = $wpdb->query($wpdb->prepare($update_sql, $params));
            if ($r !== false) { $updated_count += (int) $r; }
        }

        
        $orphan_keys      = array_diff_key($rows_by_voucher, $existing_set);
        $orphans_inserted = 0;
        foreach (array_chunk($orphan_keys, 200, true) as $chunk) {
            $values_sql = array();
            $params     = array();
            foreach ($chunk as $voucher_no => $r) {
                $values_sql[] = '(0, %s, %s, %d, ' . ($r['weight'] !== null ? '%f' : 'NULL') . ', %f, %s, %s, %s, %s, %s)';
                $params[] = $carrier;
                $params[] = (string) $voucher_no;
                $params[] = (int) $r['pieces'];
                if ($r['weight'] !== null) { $params[] = (float) $r['weight']; }
                $params[] = (float) $r['cost'];
                $params[] = 'invoice_csv';
                $params[] = 'orphan_invoice';
                $params[] = (string) $r['consignee'];
                $params[] = (string) $r['invoice_no'];
                $params[] = (string) $r['pickup_date'];
            }
            $sql = "INSERT INTO {$table}
                    (order_id, carrier, voucher_no, pieces, weight, carrier_cost,
                     cost_source, status, consignee, reference_1, reference_2)
                    VALUES " . implode(',', $values_sql) . "
                    ON DUPLICATE KEY UPDATE
                        carrier_cost = VALUES(carrier_cost),
                        cost_source  = VALUES(cost_source),
                        consignee    = VALUES(consignee),
                        reference_1  = VALUES(reference_1),
                        reference_2  = VALUES(reference_2)";
            $r = $wpdb->query($wpdb->prepare($sql, $params));
            if ($r !== false) { $orphans_inserted += count($chunk); }
        }

        return array(
            'ok'              => true,
            'parsed_rows'     => $parsed_rows,
            'unique_vouchers' => count($rows_by_voucher),
            'matched'         => $matched_count,
            'updated'         => $updated_count,
            'unmatched'       => count($orphan_keys),
            'orphans_stored'  => $orphans_inserted,
            'errors'          => array_slice($row_errors, 0, 10),
        );
    }

    public function ajax_delete_voucher() {
        $this->verify_admin_ajax();
        $voucher_id = isset($_POST['voucher_id']) ? (int) $_POST['voucher_id'] : 0;
        $ok = AIEO_Fulfillment_Vouchers::delete($voucher_id);
        if (!$ok) { wp_send_json_error(array('message' => 'delete_failed')); }
        wp_send_json_success();
    }

    

    public function ajax_create_document() {
        $this->verify_admin_ajax();
        $order_id = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0;
        $type     = isset($_POST['doc_type']) ? sanitize_key((string) wp_unslash($_POST['doc_type'])) : '';
        if ($order_id <= 0 || $type === '') {
            wp_send_json_error(array('message' => 'order_id and doc_type required'));
        }
        $order = function_exists('wc_get_order') ? wc_get_order($order_id) : null;
        if (!$order) { wp_send_json_error(array('message' => 'order_not_found')); }

        $docs = $this->documents();
        if (!isset($docs[$type])) {
            wp_send_json_error(array('message' => 'unknown_doc_type'));
        }
        $r = $docs[$type]->generate($order, array('force_regenerate' => true));
        if (empty($r['ok'])) {
            wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'generate_failed'));
        }
        $upload  = wp_upload_dir(null, false);
        $pdf_url = !empty($r['pdf_path']) ? $upload['baseurl'] . '/' . ltrim((string) $r['pdf_path'], '/') : '';
        wp_send_json_success(array(
            'document_id' => (int) $r['document_id'],
            'pdf_url'     => $pdf_url,
            
            'message'     => sprintf(__('%1$s %2$s generated.', 'ai-eshop-optimizer'), $docs[$type]->label(), isset($r['number']) ? '#' . $r['number'] : ''),
        ));
    }

    public function ajax_get_document_pdf() {
        $this->verify_admin_ajax();
        $document_id = isset($_POST['document_id']) ? (int) $_POST['document_id'] : 0;
        $regenerate  = !empty($_POST['regenerate']);

        $document = AIEO_Fulfillment_Documents::get($document_id);
        if (!$document) { wp_send_json_error(array('message' => 'document_not_found')); }

        $docs = $this->documents();
        $type = (string) $document['type'];
        if (!isset($docs[$type])) {
            wp_send_json_error(array('message' => 'unknown_doc_type'));
        }

        if ($regenerate || empty($document['pdf_path'])) {
            $order = wc_get_order((int) $document['order_id']);
            if (!$order) { wp_send_json_error(array('message' => 'order_not_found')); }
            $r = $docs[$type]->generate($order, array('force_regenerate' => true));
            if (empty($r['ok'])) {
                wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'regenerate_failed'));
            }
            $document = AIEO_Fulfillment_Documents::get($document_id);
        }
        $upload  = wp_upload_dir(null, false);
        $pdf_url = !empty($document['pdf_path']) ? $upload['baseurl'] . '/' . ltrim((string) $document['pdf_path'], '/') : '';
        if ($pdf_url === '') { wp_send_json_error(array('message' => 'no_pdf_path')); }
        
        
        AIEO_Fulfillment_Documents::mark_printed($document_id);
        wp_send_json_success(array('pdf_url' => $pdf_url));
    }

    
    public function ajax_print_doc_for_order() {
        $this->verify_admin_ajax();
        $order_id = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0;
        $type     = isset($_POST['doc_type']) ? sanitize_key((string) wp_unslash($_POST['doc_type'])) : '';
        if ($order_id <= 0 || $type === '') {
            wp_send_json_error(array('message' => 'order_id and doc_type required'));
        }
        $docs = $this->documents();
        if (!isset($docs[$type])) {
            wp_send_json_error(array('message' => 'unknown_doc_type'));
        }
        $order = wc_get_order($order_id);
        if (!$order) { wp_send_json_error(array('message' => 'order_not_found')); }

        
        
        
        
        
        
        
        
        $r = $docs[$type]->generate($order, array('force_regenerate' => true));
        if (empty($r['ok'])) {
            wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'generate_failed'));
        }
        $document_id = (int) $r['document_id'];
        $document    = AIEO_Fulfillment_Documents::get($document_id);
        if (!$document) {
            wp_send_json_error(array('message' => 'document_not_found_after_generate'));
        }
        $upload = wp_upload_dir(null, false);
        $pdf_url = !empty($document['pdf_path']) ? $upload['baseurl'] . '/' . ltrim((string) $document['pdf_path'], '/') : '';
        if ($pdf_url === '') {
            wp_send_json_error(array('message' => 'no_pdf_path'));
        }
        AIEO_Fulfillment_Documents::mark_printed($document_id);
        wp_send_json_success(array(
            'pdf_url'         => $pdf_url,
            'document_id'     => $document_id,
            'already_printed' => true,
        ));
    }

    
    public function ajax_save_manual_voucher() {
        $this->verify_admin_ajax();
        $order_id = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0;
        $carrier  = isset($_POST['carrier'])  ? sanitize_key((string) wp_unslash($_POST['carrier']))  : '';
        $vno_raw  = isset($_POST['voucher_no']) ? (string) wp_unslash($_POST['voucher_no']) : '';
        $vno      = sanitize_text_field($vno_raw);
        $notify   = !empty($_POST['notify']);
        if ($order_id <= 0 || $carrier === '' || $vno === '') {
            wp_send_json_error(array('message' => 'order_id, carrier, voucher_no required'));
        }
        
        
        
        
        $couriers = $this->manual_entry_couriers();
        if (!isset($couriers[$carrier])) {
            wp_send_json_error(array('message' => 'carrier_not_supported'));
        }
        
        
        
        
        $customer_shipping_paid = null;
        $wc_order = function_exists('wc_get_order') ? wc_get_order($order_id) : null;
        if ($wc_order instanceof WC_Order) {
            $customer_shipping_paid = (float) $wc_order->get_shipping_total();
        }

        $r = AIEO_Fulfillment_Vouchers::create(array(
            'order_id'               => $order_id,
            'carrier'                => $carrier,
            'voucher_no'             => $vno,
            'status'                 => 'created',
            'status_at'              => current_time('mysql', true),
            'customer_shipping_paid' => $customer_shipping_paid,
        ));
        if (empty($r['ok'])) {
            wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'create_failed'));
        }
        
        
        
        $email_sent = false;
        $email_err  = '';
        if ($notify) {
            try {
                $email_sent = $this->send_customer_tracking_email($order_id, $couriers[$carrier], $vno);
            } catch (\Throwable $e) {
                $email_err = $e->getMessage();
            }
        }
        
        ob_start();
        $this->orders_list_column_body('aieo_ff_voucher', $order_id);
        $cell_html = ob_get_clean();
        wp_send_json_success(array(
            'voucher_id' => (int) $r['id'],
            'cell_html'  => $cell_html,
            'email_sent' => (bool) $email_sent,
            'email_err'  => (string) $email_err,
        ));
    }

    
    public function ajax_save_carrier_map() {
        $this->verify_admin_ajax();
        $code  = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : '';
        $title = isset($_POST['title'])   ? trim((string) wp_unslash($_POST['title']))           : '';
        if ($code === '') { wp_send_json_error(array('message' => 'carrier required')); }
        if (!isset($this->manual_entry_couriers()[$code])) {
            wp_send_json_error(array('message' => 'unsupported_carrier'));
        }
        if (!class_exists('AIEO_Fulfillment_Settings')) {
            wp_send_json_error(array('message' => 'settings_unavailable'));
        }
        
        
        
        $map = $this->get_carrier_map();
        foreach ($map as $existing_title => $existing_code) {
            if ($existing_code === $code) {
                $this->set_carrier_map_entry($existing_title, '');
            }
        }
        if ($title !== '') {
            $this->set_carrier_map_entry($title, $code);
        }
        wp_send_json_success(array('mapped' => $title));
    }

    
    public function ajax_save_custom_carrier() {
        $this->verify_admin_ajax();
        $slug_raw = isset($_POST['slug'])  ? (string) wp_unslash($_POST['slug'])  : '';
        $label    = isset($_POST['label']) ? (string) wp_unslash($_POST['label']) : '';
        $tpl      = isset($_POST['tracking_url_template']) ? (string) wp_unslash($_POST['tracking_url_template']) : '';
        $title    = isset($_POST['linked_method_title']) ? trim((string) wp_unslash($_POST['linked_method_title'])) : '';
        $slug = $this->save_custom_carrier($slug_raw, $label, $tpl);
        if ($slug === '') {
            wp_send_json_error(array('message' => 'invalid_slug_or_label_or_collides_with_builtin'));
        }
        $code = AIEO_Courier_Driver_Custom::PREFIX . $slug;
        
        
        if ($title !== '') {
            $map = $this->get_carrier_map();
            foreach ($map as $t => $c) {
                if ($c === $code) { $this->set_carrier_map_entry($t, ''); }
            }
            $this->set_carrier_map_entry($title, $code);
        }
        wp_send_json_success(array(
            'slug' => $slug,
            'code' => $code,
        ));
    }

    
    public function ajax_delete_custom_carrier() {
        $this->verify_admin_ajax();
        $slug = isset($_POST['slug']) ? sanitize_key((string) wp_unslash($_POST['slug'])) : '';
        if ($slug === '') { wp_send_json_error(array('message' => 'slug required')); }
        $ok = $this->delete_custom_carrier($slug);
        if (!$ok) { wp_send_json_error(array('message' => 'not_found')); }
        wp_send_json_success(array('slug' => $slug));
    }

    
    private function render_link_title_options($current_carrier_code) {
        $titles_idx = $this->get_distinct_shipping_titles();
        $map        = $this->get_carrier_map();
        $couriers   = $this->couriers();
        $linked_title_for_current = $this->get_linked_title_for_carrier((string) $current_carrier_code);

        $html  = '';
        $html .= '<option value=""' . selected($linked_title_for_current, '', false) . '>'
              . esc_html__('— none —', 'ai-eshop-optimizer') . '</option>';

        foreach ($titles_idx as $t => $info) {
            $is_enabled_wc = !empty($info['enabled']);
            $zones_csv     = implode(', ', (array) $info['zones']);
            
            $taken_by_code  = isset($map[$t]) ? (string) $map[$t] : '';
            $taken_by_other = ($taken_by_code !== '' && $taken_by_code !== (string) $current_carrier_code);
            $taken_by_label = '';
            if ($taken_by_other) {
                $taken_by_label = isset($couriers[$taken_by_code]) && method_exists($couriers[$taken_by_code], 'label')
                    ? (string) $couriers[$taken_by_code]->label()
                    : strtoupper($taken_by_code);
            }

            $is_disabled = (!$is_enabled_wc) || $taken_by_other;
            $suffix      = '';
            if (!$is_enabled_wc) {
                $suffix = ' ' . esc_html__('(disabled in WC)', 'ai-eshop-optimizer');
            } elseif ($taken_by_other) {
                
                $suffix = ' ' . sprintf(esc_html__('(linked to %s)', 'ai-eshop-optimizer'), esc_html($taken_by_label));
            }

            $label = esc_html($t) . $suffix;
            if ($zones_csv !== '') {
                $label .= ' — ' . esc_html($zones_csv);
            }
            $is_selected = ((string) $current_carrier_code !== '' && $linked_title_for_current === $t);
            $html .= '<option value="' . esc_attr($t) . '"'
                  . ($is_selected  ? ' selected'      : '')
                  . ($is_disabled  ? ' disabled'      : '')
                  . '>' . $label . '</option>';
        }
        return $html;
    }

    
    public function get_linked_title_for_carrier($carrier_code) {
        $code = sanitize_key((string) $carrier_code);
        if ($code === '') { return ''; }
        foreach ($this->get_carrier_map() as $title => $existing) {
            if ($existing === $code) { return $title; }
        }
        return '';
    }

    
    
    

    
    public function get_custom_carriers() {
        $raw = class_exists('AIEO_Fulfillment_Settings')
            ? AIEO_Fulfillment_Settings::get('custom_carriers', array())
            : array();
        if (!is_array($raw)) { return array(); }
        $out = array();
        foreach ($raw as $slug => $cfg) {
            $slug = sanitize_key((string) $slug);
            if ($slug === '' || !is_array($cfg)) { continue; }
            $out[$slug] = array(
                'label'                 => isset($cfg['label']) ? (string) $cfg['label'] : '',
                'tracking_url_template' => isset($cfg['tracking_url_template']) ? (string) $cfg['tracking_url_template'] : '',
            );
        }

        
        $filtered = apply_filters('aieo_custom_carriers', $out);
        if (is_array($filtered)) {
            foreach ($filtered as $slug => $cfg) {
                $slug = sanitize_key((string) $slug);
                if ($slug === '' || !is_array($cfg)) { continue; }
                $out[$slug] = array(
                    'label'                 => isset($cfg['label']) ? (string) $cfg['label'] : '',
                    'tracking_url_template' => isset($cfg['tracking_url_template']) ? (string) $cfg['tracking_url_template'] : '',
                );
            }
        }
        return $out;
    }

    
    public function save_custom_carrier($slug, $label, $tracking_url_template = '') {
        if (!class_exists('AIEO_Fulfillment_Settings')) { return ''; }
        $slug = AIEO_Courier_Driver_Custom::slugify_user_input($slug);
        if ($slug === '')          { return ''; }
        $label = trim((string) $label);
        if ($label === '')         { return ''; }
        $template = trim((string) $tracking_url_template);
        $reg = $this->get_custom_carriers();
        $reg[$slug] = array(
            'label'                 => $label,
            'tracking_url_template' => $template,
        );
        AIEO_Fulfillment_Settings::set('custom_carriers', $reg);
        return $slug;
    }

    
    public function delete_custom_carrier($slug) {
        $slug = sanitize_key((string) $slug);
        if ($slug === '' || !class_exists('AIEO_Fulfillment_Settings')) { return false; }
        $reg = $this->get_custom_carriers();
        if (!isset($reg[$slug])) { return false; }
        unset($reg[$slug]);
        AIEO_Fulfillment_Settings::set('custom_carriers', $reg);
        
        
        
        $code = AIEO_Courier_Driver_Custom::PREFIX . $slug;
        $map = $this->get_carrier_map();
        foreach ($map as $title => $existing) {
            if ($existing === $code) { $this->set_carrier_map_entry($title, ''); }
        }
        return true;
    }

    
    private function send_customer_tracking_email($order_id, $driver, $voucher_no) {
        $order = wc_get_order((int) $order_id);
        if (!$order) { return false; }
        $to = (string) $order->get_billing_email();
        if ($to === '') { return false; }

        $carrier_lbl = method_exists($driver, 'label') ? (string) $driver->label() : '';
        $track_url   = method_exists($driver, 'tracking_url') ? (string) $driver->tracking_url($voucher_no) : '';
        $first_name  = (string) $order->get_billing_first_name();
        $store_name  = (string) get_bloginfo('name');
        $order_no    = (string) $order->get_order_number();

        
        $subject = sprintf(__('%1$s — your order #%2$s has shipped', 'ai-eshop-optimizer'), $store_name, $order_no);

        $body_lines = array();
        
        $body_lines[] = trim(sprintf(__('Hello %s,', 'ai-eshop-optimizer'), $first_name));
        $body_lines[] = '';
        
        $body_lines[] = sprintf(__('Your order #%1$s has been dispatched via %2$s.', 'ai-eshop-optimizer'), $order_no, $carrier_lbl !== '' ? $carrier_lbl : __('our courier', 'ai-eshop-optimizer'));
        $body_lines[] = '';
        
        $body_lines[] = sprintf(__('Tracking number: %s', 'ai-eshop-optimizer'), $voucher_no);
        if ($track_url !== '') {
            
            $body_lines[] = sprintf(__('Track your shipment: %s', 'ai-eshop-optimizer'), $track_url);
        }
        $body_lines[] = '';
        $body_lines[] = __('Thank you for your order.', 'ai-eshop-optimizer');
        $body_lines[] = $store_name;

        $body = implode("\n", $body_lines);

        
        
        $from_name  = $store_name;
        $from_email = '';
        if (function_exists('WC') && WC()->mailer()) {
            $mailer     = WC()->mailer();
            $from_name  = $mailer->get_from_name()    ?: $from_name;
            $from_email = $mailer->get_from_address() ?: '';
        }
        if ($from_email === '') { $from_email = (string) get_option('admin_email'); }

        $headers = array(
            'From: ' . $from_name . ' <' . $from_email . '>',
            'Content-Type: text/plain; charset=UTF-8',
        );

        
        
        $order->add_order_note(sprintf(
            
            __('Tracking number %1$s (%2$s) sent to customer at %3$s.', 'ai-eshop-optimizer'),
            $voucher_no, $carrier_lbl, $to
        ));

        return (bool) wp_mail($to, $subject, $body, $headers);
    }

    public function ajax_delete_document() {
        $this->verify_admin_ajax();
        $document_id = isset($_POST['document_id']) ? (int) $_POST['document_id'] : 0;
        $ok = AIEO_Fulfillment_Documents::delete($document_id);
        if (!$ok) { wp_send_json_error(array('message' => 'delete_failed')); }
        wp_send_json_success();
    }

    

    private function read_bulk_payload() {
        $type      = isset($_POST['doc_type']) ? sanitize_key((string) wp_unslash($_POST['doc_type'])) : '';
        $date_from = isset($_POST['date_from']) ? sanitize_text_field((string) wp_unslash($_POST['date_from'])) : '';
        $date_to   = isset($_POST['date_to'])   ? sanitize_text_field((string) wp_unslash($_POST['date_to']))   : '';
        $statuses_csv = isset($_POST['statuses']) ? sanitize_text_field((string) wp_unslash($_POST['statuses'])) : 'wc-completed,wc-processing';
        $statuses = array_filter(array_map('sanitize_key', explode(',', $statuses_csv)));
        if (empty($statuses)) { $statuses = array('wc-completed', 'wc-processing'); }
        return compact('type', 'date_from', 'date_to', 'statuses');
    }

    public function ajax_bulk_merge() {
        $this->verify_admin_ajax();
        $p = $this->read_bulk_payload();
        if ($p['type'] === '') { wp_send_json_error(array('message' => 'doc_type required')); }
        $order_ids = AIEO_Fulfillment_Bulk::order_ids_in_range($p['date_from'], $p['date_to'], $p['statuses']);
        if (empty($order_ids)) { wp_send_json_error(array('message' => 'no_orders_in_range')); }
        $r = AIEO_Fulfillment_Bulk::merge_documents($order_ids, $p['type']);
        if (empty($r['ok'])) { wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'merge_failed')); }
        
        
        foreach ($order_ids as $oid) {
            $existing = AIEO_Fulfillment_Documents::get_for_order_by_type((int) $oid, $p['type']);
            if ($existing && !empty($existing['id'])) {
                AIEO_Fulfillment_Documents::mark_printed((int) $existing['id']);
            }
        }
        wp_send_json_success(array(
            'pdf_url' => isset($r['pdf_url']) ? (string) $r['pdf_url'] : '',
            'merged'  => isset($r['merged']) ? (int) $r['merged'] : 0,
            
            'message' => sprintf(__('Merged %d documents.', 'ai-eshop-optimizer'), (int) ($r['merged'] ?? 0)),
        ));
    }

    public function ajax_bulk_zip() {
        $this->verify_admin_ajax();
        $p = $this->read_bulk_payload();
        if ($p['type'] === '') { wp_send_json_error(array('message' => 'doc_type required')); }
        $order_ids = AIEO_Fulfillment_Bulk::order_ids_in_range($p['date_from'], $p['date_to'], $p['statuses']);
        if (empty($order_ids)) { wp_send_json_error(array('message' => 'no_orders_in_range')); }
        $r = AIEO_Fulfillment_Bulk::zip_documents($order_ids, $p['type']);
        if (empty($r['ok'])) { wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'zip_failed')); }
        
        foreach ($order_ids as $oid) {
            $existing = AIEO_Fulfillment_Documents::get_for_order_by_type((int) $oid, $p['type']);
            if ($existing && !empty($existing['id'])) {
                AIEO_Fulfillment_Documents::mark_printed((int) $existing['id']);
            }
        }
        wp_send_json_success(array(
            'zip_url' => (string) $r['zip_url'],
            'count'   => (int) $r['count'],
        ));
    }

    public function ajax_bulk_zip_labels() {
        $this->verify_admin_ajax();
        $carrier   = isset($_POST['carrier'])   ? sanitize_key((string) wp_unslash($_POST['carrier'])) : '';
        $date_from = isset($_POST['date_from']) ? sanitize_text_field((string) wp_unslash($_POST['date_from'])) : '';
        $date_to   = isset($_POST['date_to'])   ? sanitize_text_field((string) wp_unslash($_POST['date_to']))   : '';
        $voucher_ids = AIEO_Fulfillment_Bulk::voucher_ids_with_labels_in_range($date_from, $date_to, $carrier);
        if (empty($voucher_ids)) { wp_send_json_error(array('message' => 'no_labels_cached')); }
        $r = AIEO_Fulfillment_Bulk::zip_voucher_labels($voucher_ids);
        if (empty($r['ok'])) { wp_send_json_error(array('message' => isset($r['error']) ? $r['error'] : 'zip_failed')); }
        wp_send_json_success(array('zip_url' => (string) $r['zip_url'], 'count' => (int) $r['count']));
    }

    

    public function ajax_migrate_audit() {
        $this->verify_admin_ajax();
        wp_send_json_success(AIEO_Fulfillment_Migrator::audit());
    }

    public function ajax_migrate_batch() {
        $this->verify_admin_ajax();
        $step       = isset($_POST['step'])       ? sanitize_key((string) wp_unslash($_POST['step'])) : '';
        $batch_size = isset($_POST['batch_size']) ? (int) $_POST['batch_size'] : 500;
        if ($step === '') { wp_send_json_error(array('message' => 'step required')); }
        $r = AIEO_Fulfillment_Migrator::run_batch($step, $batch_size);
        wp_send_json_success($r);
    }

    public function ajax_migrate_reset() {
        $this->verify_admin_ajax();
        $step = isset($_POST['step']) ? sanitize_key((string) wp_unslash($_POST['step'])) : null;
        AIEO_Fulfillment_Migrator::reset_progress($step ?: null);
        wp_send_json_success();
    }

    

    
    public function orders_list_columns($columns) {
        if (!is_array($columns)) { return $columns; }
        $new = array();
        foreach ($columns as $key => $label) {
            $new[$key] = $label;
            
            if ($key === 'order_status') {
                $new['aieo_ff_voucher'] = __('Voucher', 'ai-eshop-optimizer');
            }
        }
        
        if (!isset($new['aieo_ff_voucher'])) {
            $new['aieo_ff_voucher'] = __('Voucher', 'ai-eshop-optimizer');
        }
        return $new;
    }

    
    public function orders_list_column_body($column, $order_or_post_id) {
        if ($column !== 'aieo_ff_voucher') { return; }

        $order_id = is_object($order_or_post_id) && method_exists($order_or_post_id, 'get_id')
            ? (int) $order_or_post_id->get_id()
            : (int) $order_or_post_id;
        if ($order_id <= 0) { echo '—'; return; }

        echo '<div class="aieo-ff-doc-stack" style="display:flex;flex-direction:column;gap:3px;align-items:flex-start;">';
        
        $ps_printed = class_exists('AIEO_Fulfillment_Documents')
            && AIEO_Fulfillment_Documents::has_printed_for_order($order_id, 'packing-slip');
        $this->render_ff_doc_button($order_id, 'packing-slip', __('Packing Slip', 'ai-eshop-optimizer'), $ps_printed);

        
        $this->render_ff_voucher_block($order_id);

        
        $inv_printed = class_exists('AIEO_Fulfillment_Documents')
            && AIEO_Fulfillment_Documents::has_printed_for_order($order_id, 'invoice');
        $this->render_ff_doc_button($order_id, 'invoice', __('Invoice', 'ai-eshop-optimizer'), $inv_printed);
        echo '</div>';
    }

    
    private function render_ff_doc_button($order_id, $type, $label, $printed) {
        $style = $printed
            ? 'background:#dcefdc;border-color:#7ad03a;color:#1d2327;'  
            : '';
        printf(
            '<button type="button" class="button button-small aieo-ff-print-doc%s" data-order-id="%d" data-doc-type="%s" style="font-size:11px;%s" title="%s">＋ %s</button>',
            $printed ? ' is-printed' : '',
            (int) $order_id,
            esc_attr($type),
            esc_attr($style),
            esc_attr($printed
                ? sprintf( __('%s already printed — click to re-print', 'ai-eshop-optimizer'), $label)
                : sprintf( __('Generate + print %s', 'ai-eshop-optimizer'), $label)),
            esc_html($label)
        );
    }

    
    public function orders_list_voucher_modal() {
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        if (!$screen) { return; }
        $hpos   = function_exists('wc_get_page_screen_id') ? wc_get_page_screen_id('shop-order') : '';
        if (!in_array($screen->id, array($hpos, 'edit-shop_order'), true)) { return; }
        $couriers = $this->linked_carriers();
        if (empty($couriers)) { return; }
        ?>
        <div id="aieo-ff-voucher-modal" class="aieo-ff-voucher-modal" style="display:none;position:fixed;inset:0;z-index:100000;align-items:center;justify-content:center;">
            <div class="aieo-ff-voucher-modal__backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,.45);"></div>
            <div class="aieo-ff-voucher-modal__dialog" style="position:relative;background:#fff;border-radius:6px;box-shadow:0 10px 40px rgba(0,0,0,.25);width:min(440px,90vw);padding:18px 20px;">
                <h2 style="margin:0 0 12px;font-size:16px;"><?php esc_html_e('Add tracking number', 'ai-eshop-optimizer'); ?></h2>
                <p class="description" style="margin:0 0 14px;"><?php esc_html_e('Order #', 'ai-eshop-optimizer'); ?><span class="aieo-ff-voucher-modal__order"></span></p>

                <label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;"><?php esc_html_e('Carrier', 'ai-eshop-optimizer'); ?></label>
                <select id="aieo-ff-voucher-modal-carrier" style="width:100%;margin-bottom:12px;">
                    <?php foreach ($couriers as $code => $driver):
                        $lbl = method_exists($driver, 'label') ? $driver->label() : strtoupper($code);
                    ?>
                        <option value="<?php echo esc_attr($code); ?>"><?php echo esc_html($lbl); ?></option>
                    <?php endforeach; ?>
                </select>

                <label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px;"><?php esc_html_e('Tracking number', 'ai-eshop-optimizer'); ?></label>
                <input type="text" id="aieo-ff-voucher-modal-no" placeholder="<?php esc_attr_e('e.g. 1234567890', 'ai-eshop-optimizer'); ?>" style="width:100%;font-family:monospace;margin-bottom:12px;" />

                <label style="display:flex;gap:8px;align-items:center;font-size:13px;margin-bottom:14px;">
                    <input type="checkbox" id="aieo-ff-voucher-modal-notify" checked />
                    <?php esc_html_e('Notify customer (send email with tracking link)', 'ai-eshop-optimizer'); ?>
                </label>

                <div id="aieo-ff-voucher-modal-err" style="display:none;color:#b91c1c;font-size:12px;margin-bottom:10px;"></div>

                <div style="display:flex;gap:8px;justify-content:flex-end;">
                    <button type="button" class="button" id="aieo-ff-voucher-modal-cancel"><?php esc_html_e('Cancel', 'ai-eshop-optimizer'); ?></button>
                    <button type="button" class="button button-primary" id="aieo-ff-voucher-modal-save"><?php esc_html_e('Save', 'ai-eshop-optimizer'); ?></button>
                </div>
            </div>
        </div>
        <?php
    }

    
    private function render_ff_voucher_block($order_id) {
        $vouchers = AIEO_Fulfillment_Vouchers::get_for_order($order_id);
        if (empty($vouchers)) {
            
            
            
            
            
            $couriers = $this->linked_carriers();
            if (empty($couriers)) { return; }
            
            
            $preselect = $this->get_carrier_for_order($order_id);
            echo '<button type="button" class="button button-small aieo-ff-voucher-trigger" data-order-id="' . (int) $order_id . '"'
                . ($preselect !== '' ? ' data-preselect-carrier="' . esc_attr($preselect) . '"' : '')
                . ' style="font-size:11px;">＋ ' . esc_html__('Voucher', 'ai-eshop-optimizer') . '</button>';
            return;
        }

        $couriers = $this->couriers();
        $upload   = wp_upload_dir(null, false);
        $base_url = (string) $upload['baseurl'];

        echo '<div class="aieo-ff-voucher-cell" style="display:flex;flex-direction:column;gap:3px;">';
        foreach ($vouchers as $v) {
            $code        = (string) $v['carrier'];
            $vno         = (string) $v['voucher_no'];
            $driver      = isset($couriers[$code]) ? $couriers[$code] : null;
            $carrier_lbl = $driver ? $driver->label() : strtoupper($code);
            $tracking    = $driver ? $driver->tracking_url($vno) : '';
            $label_pdf   = !empty($v['label_pdf_path']) ? $base_url . '/' . ltrim((string) $v['label_pdf_path'], '/') : '';
            $vno_short   = strlen($vno) > 12 ? '…' . substr($vno, -8) : $vno;

            echo '<div style="font-size:11px;line-height:1.4;display:flex;align-items:center;gap:4px;">';
            
            $status_color = ($v['status'] === 'delivered') ? '#15803d'
                : (($v['status'] === 'returned' || $v['status'] === 'canceled' || $v['status'] === 'failed') ? '#b91c1c' : '#6b7280');
            echo '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:' . esc_attr($status_color) . ';" title="' . esc_attr((string) $v['status']) . '"></span>';
            echo '<strong>' . esc_html($carrier_lbl) . '</strong> ';
            if ($tracking !== '') {
                echo '<a href="' . esc_url($tracking) . '" target="_blank" rel="noopener" title="' . esc_attr($vno) . '"><code style="font-size:10px;">' . esc_html($vno_short) . '</code></a>';
            } else {
                echo '<code style="font-size:10px;">' . esc_html($vno_short) . '</code>';
            }
            if ($label_pdf !== '') {
                
                
                echo ' <a href="' . esc_url($label_pdf) . '" target="_blank" rel="noopener" class="button button-small" style="padding:0 6px;font-size:10px;line-height:18px;height:20px;" title="' . esc_attr__('Print label', 'ai-eshop-optimizer') . '">🖨</a>';
            } else if ($driver && $driver->has_capability('print')) {
                
                
                
                $edit_url = function_exists('wc_get_page_screen_id') && wc_get_page_screen_id('shop-order') === get_current_screen()->id
                    ? admin_url('admin.php?page=wc-orders&action=edit&id=' . $order_id . '#aieo_fulfillment_meta_box')
                    : admin_url('post.php?post=' . $order_id . '&action=edit#aieo_fulfillment_meta_box');
                echo ' <a href="' . esc_url($edit_url) . '" class="button button-small" style="padding:0 6px;font-size:10px;line-height:18px;height:20px;" title="' . esc_attr__('Fetch + print', 'ai-eshop-optimizer') . '">🖨</a>';
            }
            echo '</div>';
        }
        echo '</div>';
    }

    
    public function render_channel_filter_dropdown($order_type = 'shop_order', $which = 'top') {
        if ($which !== 'top') { return; }
        if ($order_type !== 'shop_order') { return; }
        $this->render_channel_filter_select();
    }

    
    public function render_channel_filter_dropdown_legacy() {
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        if (!$screen || $screen->id !== 'edit-shop_order') { return; }
        $this->render_channel_filter_select();
    }

    
    private function render_channel_filter_select() {
        $current = isset($_GET['aieo_channel']) ? sanitize_text_field(wp_unslash((string) $_GET['aieo_channel'])) : '';

        $marketplaces = $this->list_marketplace_filter_options();
        $couriers     = $this->list_courier_filter_options();
        $pickups      = $this->list_pickup_filter_options();

        echo '<select name="aieo_channel" id="aieo-channel-filter" style="margin-left:6px;">';
        echo '<option value="">' . esc_html__('All marketplaces & couriers', 'ai-eshop-optimizer') . '</option>';

        if (!empty($marketplaces)) {
            echo '<optgroup label="' . esc_attr__('Marketplaces', 'ai-eshop-optimizer') . '">';
            foreach ($marketplaces as $slug => $label) {
                $val = 'm:' . $slug;
                printf(
                    '<option value="%s" %s>%s</option>',
                    esc_attr($val),
                    selected($current, $val, false),
                    esc_html($label)
                );
            }
            echo '</optgroup>';
        }

        if (!empty($couriers)) {
            echo '<optgroup label="' . esc_attr__('Couriers', 'ai-eshop-optimizer') . '">';
            foreach ($couriers as $slug => $label) {
                $val = 'c:' . $slug;
                printf(
                    '<option value="%s" %s>%s</option>',
                    esc_attr($val),
                    selected($current, $val, false),
                    esc_html($label)
                );
            }
            echo '</optgroup>';
        }

        if (!empty($pickups)) {
            echo '<optgroup label="' . esc_attr__('Store pickup', 'ai-eshop-optimizer') . '">';
            foreach ($pickups as $name => $label) {
                $val = 'p:' . $name;
                printf(
                    '<option value="%s" %s>%s</option>',
                    esc_attr($val),
                    selected($current, $val, false),
                    esc_html($label)
                );
            }
            echo '</optgroup>';
        }

        echo '</select>';
    }

    
    private function list_pickup_filter_options() {
        $raw = get_option('pickup_location_pickup_locations', array());
        $out = array();
        if (is_array($raw)) {
            foreach ($raw as $l) {
                if (is_array($l) && !empty($l['name'])) {
                    $name = (string) $l['name'];
                    $out[$name] = $name;
                }
            }
        }
        return $out;
    }

    
    private function list_marketplace_filter_options() {
        global $wpdb;
        $vendor_slugs = $wpdb->get_col(
            "SELECT DISTINCT meta_value FROM {$wpdb->postmeta}
              WHERE meta_key = '_aieo_moi_vendor_slug' AND meta_value <> ''
              ORDER BY meta_value ASC"
        );
        if (empty($vendor_slugs) && class_exists('AIEO_MOI_Vendor_Manager') && method_exists('AIEO_MOI_Vendor_Manager', 'list_all')) {
            $vendors = (array) AIEO_MOI_Vendor_Manager::list_all();
            foreach ($vendors as $v) {
                if (!empty($v['slug'])) { $vendor_slugs[] = $v['slug']; }
            }
        }
        $out = array();
        foreach ((array) $vendor_slugs as $slug) {
            $slug = (string) $slug;
            if ($slug === '') { continue; }
            
            $out[$slug] = ucwords(str_replace(array('-', '_'), ' ', $slug));
        }
        return $out;
    }

    
    private function list_courier_filter_options() {
        $out = array();
        
        foreach ($this->active_couriers() as $slug => $entry) {
            $driver = isset($entry['driver']) ? $entry['driver'] : null;
            $label  = ($driver && method_exists($driver, 'label')) ? (string) $driver->label() : ucwords(str_replace('_', ' ', (string) $slug));
            $out[(string) $slug] = $label;
        }
        
        
        
        global $wpdb;
        $used = (array) $wpdb->get_col("SELECT DISTINCT carrier FROM {$wpdb->prefix}aieo_dmm_fulfillment_vouchers WHERE carrier <> '' AND carrier <> 'manual'");
        $all_drivers = $this->couriers();
        foreach ($used as $slug) {
            $slug = (string) $slug;
            if ($slug === '' || isset($out[$slug])) { continue; }
            $label = isset($all_drivers[$slug]) && method_exists($all_drivers[$slug], 'label')
                ? (string) $all_drivers[$slug]->label()
                : ucwords(str_replace('_', ' ', $slug));
            $out[$slug] = $label;
        }
        ksort($out);
        return $out;
    }

    
    private function resolve_orders_for_courier($courier_slug) {
        global $wpdb;
        $courier_slug = (string) $courier_slug;
        if ($courier_slug === '') { return array(); }

        $vt = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $oi = $wpdb->prefix . 'woocommerce_order_items';
        $oim = $wpdb->prefix . 'woocommerce_order_itemmeta';

        
        $name_parts = array();
        foreach ($this->courier_name_patterns_for_slug($courier_slug) as $p) {
            $name_parts[] = $wpdb->prepare("oi.order_item_name LIKE %s", '%' . $wpdb->esc_like($p) . '%');
        }
        $name_clause = !empty($name_parts) ? '(' . implode(' OR ', $name_parts) . ')' : '0';

        
        $map_raw = (string) $wpdb->get_var($wpdb->prepare(
            "SELECT setting_value FROM {$wpdb->prefix}aieo_dmm_fulfillment_settings WHERE setting_key = %s",
            'shipping_method_carrier_map'
        ));
        $map = $map_raw !== '' ? json_decode($map_raw, true) : array();
        $instance_ids = array();
        if (is_array($map)) {
            foreach ($map as $label => $entry) {
                if (is_array($entry) && (string) ($entry['carrier'] ?? '') === $courier_slug && !empty($entry['instance_id'])) {
                    $instance_ids[] = (int) $entry['instance_id'];
                }
            }
        }
        $inst_clause = '0';
        if (!empty($instance_ids)) {
            $placeholders = implode(',', array_fill(0, count($instance_ids), '%d'));
            $inst_clause = $wpdb->prepare(
                "EXISTS (SELECT 1 FROM {$oim} m
                          WHERE m.order_item_id = oi.order_item_id
                            AND m.meta_key = 'instance_id'
                            AND m.meta_value IN ({$placeholders}))",
                ...$instance_ids
            );
        }

        
        
        $sql = "SELECT order_id FROM (
                  SELECT v.order_id
                    FROM {$vt} v
                    WHERE v.carrier = %s AND v.order_id > 0
                  UNION
                  SELECT oi.order_id
                    FROM {$oi} oi
                    WHERE oi.order_item_type = 'shipping'
                      AND ({$name_clause} OR {$inst_clause})
                ) AS x
                ORDER BY order_id DESC
                LIMIT 50000";
        
        $ids = $wpdb->get_col($wpdb->prepare($sql, $courier_slug));
        return array_values(array_map('intval', (array) $ids));
    }

    
    private function courier_name_patterns_for_slug($slug) {
        $map = array(
            'geniki'   => array('Γενικ', 'Geniki', 'Ταχυδρομικ', 'Taxydromik'),
            'elta'     => array('ELTA', 'ΕΛΤΑ', 'Ταχυμεταφορες ΕΛΤΑ'),
            'acs'      => array('ACS'),
            'speedex'  => array('Speedex', 'SPEEDEX'),
            'boxnow'   => array('Box Now', 'BoxNow', 'BOX NOW'),
            'courier_center' => array('Courier Center'),
        );
        $map = apply_filters('aieo_courier_name_patterns', $map);
        $slug = (string) $slug;
        return isset($map[$slug]) && is_array($map[$slug]) ? $map[$slug] : array();
    }

    
    public function apply_channel_filter_to_query($query_args) {
        $filter = isset($_GET['aieo_channel']) ? sanitize_text_field(wp_unslash((string) $_GET['aieo_channel'])) : '';
        if ($filter === '' || strpos($filter, ':') === false) { return $query_args; }
        list($type, $slug) = explode(':', $filter, 2);

        if ($type === 'm') {
            
            if (!isset($query_args['meta_query']) || !is_array($query_args['meta_query'])) {
                $query_args['meta_query'] = array();
            }
            $query_args['meta_query'][] = array(
                'key'   => '_aieo_moi_vendor_slug',
                'value' => $slug,
            );
        } elseif ($type === 'c') {
            
            $ids = $this->resolve_orders_for_courier($slug);
            
            
            $query_args['post__in'] = !empty($ids) ? $ids : array(0);
        } elseif ($type === 'p') {
            
            if (!isset($query_args['meta_query']) || !is_array($query_args['meta_query'])) {
                $query_args['meta_query'] = array();
            }
            $query_args['meta_query'][] = array(
                'key'   => '_aieo_pickup_location',
                'value' => $slug,
            );
        }
        return $query_args;
    }

    
    public function apply_channel_filter_to_legacy_query($query) {
        if (!is_admin() || !function_exists('get_current_screen')) { return; }
        $screen = get_current_screen();
        if (!$screen || $screen->id !== 'edit-shop_order') { return; }
        if (!$query->is_main_query()) { return; }
        $filter = isset($_GET['aieo_channel']) ? sanitize_text_field(wp_unslash((string) $_GET['aieo_channel'])) : '';
        if ($filter === '' || strpos($filter, ':') === false) { return; }
        list($type, $slug) = explode(':', $filter, 2);

        if ($type === 'm') {
            $mq = (array) $query->get('meta_query');
            $mq[] = array('key' => '_aieo_moi_vendor_slug', 'value' => $slug);
            $query->set('meta_query', $mq);
        } elseif ($type === 'c') {
            $ids = $this->resolve_orders_for_courier($slug);
            $query->set('post__in', !empty($ids) ? $ids : array(0));
        } elseif ($type === 'p') {
            $mq = (array) $query->get('meta_query');
            $mq[] = array('key' => '_aieo_pickup_location', 'value' => $slug);
            $query->set('meta_query', $mq);
        }
    }

    
    public function orders_list_bulk_actions($actions) {
        if (!is_array($actions)) { $actions = array(); }
        $actions['aieo_ff_print_packing_slips'] = __('Print packing slips (merged PDF)', 'ai-eshop-optimizer');
        $actions['aieo_ff_print_invoices']      = __('Print invoices (merged PDF)',      'ai-eshop-optimizer');
        $actions['aieo_ff_print_labels']        = __('Print picking labels (ZIP)',       'ai-eshop-optimizer');
        
        
        
        
        
        $actions['aieo_ff_print_labels_a4x3']   = __('Print labels (A4×3 portrait, 3 stacked)', 'ai-eshop-optimizer');
        
        
        
        $actions['aieo_ff_generate_vouchers']        = __('Generate vouchers for selected (no print)', 'ai-eshop-optimizer');
        $actions['aieo_ff_generate_print_complete']  = __('Generate + Print A4×3 + Mark Completed', 'ai-eshop-optimizer');
        $actions['aieo_ff_mark_completed']           = __('Mark orders Completed', 'ai-eshop-optimizer');
        return $actions;
    }

    
    public function maybe_render_a4x3_print_page() {
        if (empty($_GET['aieo_ff_print_a4x3'])) { return; }
        if (!current_user_can('manage_woocommerce')) { wp_die(esc_html__('You do not have permission to view this page.', 'ai-eshop-optimizer'), 403); }
        $token = sanitize_text_field(wp_unslash((string) $_GET['aieo_ff_print_a4x3']));
        if ($token === '' || !preg_match('/^[A-Za-z0-9]{8,40}$/', $token)) { wp_die(esc_html__('Invalid print token.', 'ai-eshop-optimizer'), 400); }
        $bundle = get_transient('aieo_ff_a4x3_' . $token);
        if (!is_array($bundle) || empty($bundle['urls'])) { wp_die(esc_html__('Print bundle expired or not found. Re-run the bulk action.', 'ai-eshop-optimizer'), 410); }

        $urls    = array_values(array_filter((array) $bundle['urls']));
        $missing = (array) ($bundle['missing'] ?? array());
        nocache_headers();
        header('Content-Type: text/html; charset=UTF-8');
        $title = sprintf(
            
            esc_html__('Print %d labels (A4×3 portrait)', 'ai-eshop-optimizer'),
            count($urls)
        );
        $btn_print  = esc_html__('Print', 'ai-eshop-optimizer');
        $loading    = esc_html__('Loading PDFs…', 'ai-eshop-optimizer');
        $done_msg   = esc_html__('All loaded — printing.', 'ai-eshop-optimizer');
        $missing_n  = count($missing);
        $missing_t  = $missing_n > 0
            ? sprintf(
                
                esc_html__('%1$d order(s) had no voucher PDF and were skipped: %2$s', 'ai-eshop-optimizer'),
                $missing_n,
                esc_html(implode(', ', array_map('intval', $missing)))
            )
            : '';
        $urls_json = wp_json_encode($urls);
        ?><!DOCTYPE html>
<html lang="el">
<head>
<meta charset="UTF-8">
<title><?php echo $title; ?></title>
<style>
@page { size: A4 portrait; margin: 5mm; }
html, body { margin: 0; padding: 0; background:#f4f5f7; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; }
.toolbar { padding: 12px 16px; background:#fff; border-bottom:1px solid #e5e7eb; position:sticky; top:0; z-index:10; display:flex; gap:12px; align-items:center; }
.toolbar button { padding: 8px 16px; background:#2271b1; color:#fff; border:0; border-radius:3px; cursor:pointer; font-size:13px; }
.toolbar button:hover { background:#135e96; }
.toolbar .status { color:#6b7280; }
.toolbar .warn { color:#92400e; background:#fef3c7; padding:4px 8px; border-radius:3px; font-size:12px; }
.pages { padding: 8px; }
.page { width: 200mm; height: 287mm; margin: 0 auto 8px; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.08); display:flex; flex-direction:column; }
.slot { flex: 1 1 33.333%; max-height: 95mm; overflow:hidden; border-bottom: 1px dashed #d1d5db; padding: 2mm; box-sizing: border-box; display:flex; align-items:center; justify-content:center; }
.slot:last-child { border-bottom: none; }
.slot canvas { display:block; max-width: 100%; max-height: 100%; }
.slot.error { color:#b91c1c; font-size:11px; }
@media print {
  body { background: transparent; }
  .toolbar { display: none !important; }
  .pages { padding: 0; }
  .page { margin: 0; box-shadow: none; page-break-after: always; }
  .page:last-child { page-break-after: auto; }
}
</style>
</head>
<body>
<div class="toolbar">
  <button type="button" onclick="window.print()"><?php echo $btn_print; ?></button>
  <span class="status" id="aieo-ff-status"><?php echo $loading; ?></span>
  <?php if ($missing_t !== '') : ?><span class="warn"><?php echo $missing_t; ?></span><?php endif; ?>
</div>
<div class="pages" id="aieo-ff-pages"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
(function () {
  var urls = <?php echo $urls_json; ?>;
  var pagesEl = document.getElementById('aieo-ff-pages');
  var statusEl = document.getElementById('aieo-ff-status');
  pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';

  function makePage() {
    var p = document.createElement('div'); p.className = 'page'; pagesEl.appendChild(p); return p;
  }
  function makeSlot(parent) {
    var s = document.createElement('div'); s.className = 'slot'; parent.appendChild(s); return s;
  }

  (async function () {
    var currentPage = null;
    for (var i = 0; i < urls.length; i++) {
      if (i % 3 === 0) { currentPage = makePage(); }
      var slot = makeSlot(currentPage);
      try {
        statusEl.textContent = '<?php echo esc_js(__('Loading', 'ai-eshop-optimizer')); ?> ' + (i + 1) + ' / ' + urls.length + '…';
        var pdf  = await pdfjsLib.getDocument(urls[i]).promise;
        var page = await pdf.getPage(1);
        var viewport = page.getViewport({ scale: 1 });
        // Scale to fit the slot (200mm wide × ~95mm tall). 3.78 px per mm at 96dpi.
        var pxW = 200 * 3.78, pxH = 95 * 3.78;
        var scale = Math.min(pxW / viewport.width, pxH / viewport.height) * 2; // *2 for sharpness
        viewport = page.getViewport({ scale: scale });
        var canvas = document.createElement('canvas');
        canvas.width = viewport.width; canvas.height = viewport.height;
        canvas.style.width = (viewport.width / 2) + 'px';
        canvas.style.height = (viewport.height / 2) + 'px';
        slot.appendChild(canvas);
        await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport }).promise;
      } catch (e) {
        slot.className = 'slot error';
        slot.textContent = '⚠ ' + urls[i];
      }
    }
    // Pad the last page to 3 slots so the dashed cut-lines look right.
    if (currentPage && currentPage.children.length < 3) {
      var need = 3 - currentPage.children.length;
      for (var j = 0; j < need; j++) { makeSlot(currentPage); }
    }
    statusEl.textContent = '<?php echo esc_js($done_msg); ?>';
    // Auto-open print dialog so workflow is: select → bulk → print → done.
    setTimeout(function () { window.print(); }, 300);
  })();
})();
</script>
</body>
</html><?php
        exit;
    }

    
    public function orders_list_handle_bulk($redirect_to, $action, $order_ids) {
        if (!current_user_can('manage_woocommerce')) { return $redirect_to; }
        if (empty($order_ids)) { return $redirect_to; }
        $order_ids = array_values(array_filter(array_map('intval', (array) $order_ids)));

        
        if ($action === 'aieo_ff_mark_completed') {
            
            
            
            
            
            
            AIEO_Fulfillment_Bulk::raise_limits(300);
            $changed = 0; $skipped = 0;
            $skip_states = array('completed', 'refunded', 'cancelled', 'failed', 'trash');
            foreach ($order_ids as $oid) {
                $o = wc_get_order($oid);
                if (!$o) { $skipped++; continue; }
                if (in_array((string) $o->get_status(), $skip_states, true)) { $skipped++; continue; }
                $o->update_status('completed', __('Bulk-marked completed from orders list.', 'ai-eshop-optimizer'));
                $changed++;
                unset($o);
                if (function_exists('wp_cache_flush_runtime')) {
                    wp_cache_flush_runtime();
                }
            }
            return add_query_arg(array('aieo_ff_bulk_msg' => 'completed', 'aieo_ff_bulk_n' => $changed, 'aieo_ff_bulk_skipped' => $skipped), $redirect_to);
        }

        
        
        
        
        if ($action === 'aieo_ff_generate_vouchers' || $action === 'aieo_ff_generate_print_complete') {
            
            
            
            
            
            AIEO_Fulfillment_Bulk::raise_limits(600);
            $channel = isset($_GET['aieo_channel']) ? sanitize_text_field(wp_unslash((string) $_GET['aieo_channel'])) : '';
            if (strpos($channel, 'c:') !== 0) {
                return add_query_arg(array('aieo_ff_bulk_msg' => 'no_carrier_filter'), $redirect_to);
            }
            $carrier_slug = substr($channel, 2);
            $couriers = $this->couriers();
            if (!isset($couriers[$carrier_slug])) {
                return add_query_arg(array('aieo_ff_bulk_msg' => 'unknown_carrier', 'aieo_ff_bulk_carrier' => $carrier_slug), $redirect_to);
            }
            $driver = $couriers[$carrier_slug];
            $create_mode = method_exists($driver, 'create_mode') ? (string) $driver->create_mode() : 'api';
            if ($create_mode !== 'api') {
                return add_query_arg(array('aieo_ff_bulk_msg' => 'carrier_no_api', 'aieo_ff_bulk_carrier' => $carrier_slug), $redirect_to);
            }

            
            
            
            global $wpdb;
            $cfg_raw = (string) $wpdb->get_var($wpdb->prepare(
                "SELECT setting_value FROM {$wpdb->prefix}aieo_dmm_fulfillment_settings WHERE setting_key = %s",
                'courier_' . $carrier_slug
            ));
            $cfg = $cfg_raw !== '' ? json_decode($cfg_raw, true) : array();
            $cfg = is_array($cfg) ? $cfg : array();
            $def_pieces   = isset($cfg['default_pieces'])   ? max(1, (int) $cfg['default_pieces'])   : 1;
            $def_weight   = isset($cfg['default_weight'])   ? (float) $cfg['default_weight']         : 0.5;
            $def_services = isset($cfg['default_services']) ? (string) $cfg['default_services']      : '';

            $created = 0; $skipped = 0; $errors = array();
            $print_voucher_ids = array(); 
            foreach ($order_ids as $oid) {
                $order = wc_get_order($oid);
                if (!$order) { $skipped++; continue; }

                
                
                $has_existing = false;
                foreach (AIEO_Fulfillment_Vouchers::get_for_order($oid) as $v) {
                    if ((string) $v['carrier'] === $carrier_slug) {
                        $has_existing = true;
                        $print_voucher_ids[] = (int) $v['id'];
                        break;
                    }
                }
                if ($has_existing) { $skipped++; continue; }

                
                $payload = $this->build_voucher_payload($order);
                $payload['pieces']       = $def_pieces;
                $payload['weight']       = $def_weight;
                $payload['cod_amount']   = 0.0;
                $payload['services_csv'] = $def_services;

                $result = $driver->create_voucher($payload);
                if (empty($result['ok'])) {
                    $errors[] = $oid . ':' . (isset($result['error']) ? substr((string) $result['error'], 0, 60) : 'err');
                    continue;
                }
                $vno = (string) ($result['voucher_no'] ?? '');
                if ($vno === '') { $errors[] = $oid . ':no_vno'; continue; }

                $stored = AIEO_Fulfillment_Vouchers::create(array(
                    'order_id'    => $oid,
                    'carrier'     => $carrier_slug,
                    'voucher_no'  => $vno,
                    'pieces'      => $def_pieces,
                    'weight'      => $def_weight,
                    'services_csv'=> $def_services,
                    'status'      => 'created',
                    'status_at'   => current_time('mysql', true),
                    'customer_shipping_paid' => (float) $order->get_shipping_total(),
                ));
                if (!empty($stored['ok'])) {
                    $created++;
                    $print_voucher_ids[] = (int) $stored['id'];
                }
                
                
                
                unset($order, $result, $stored, $payload);
                if (function_exists('wp_cache_flush_runtime')) {
                    wp_cache_flush_runtime();
                }
            }

            
            if ($action === 'aieo_ff_generate_vouchers') {
                return add_query_arg(array(
                    'aieo_ff_bulk_msg'      => 'vouchers_generated',
                    'aieo_ff_bulk_n'        => $created,
                    'aieo_ff_bulk_skipped'  => $skipped,
                    'aieo_ff_bulk_errors'   => count($errors),
                    'aieo_ff_bulk_carrier'  => $carrier_slug,
                ), $redirect_to);
            }

            
            
            
            $upload_dir = wp_upload_dir();
            $base_url   = trailingslashit($upload_dir['baseurl']);
            $pdf_urls   = array();
            $missing    = array();
            $skip_status = array('completed', 'refunded', 'cancelled', 'failed', 'trash');
            $completed_n = 0;
            foreach ($order_ids as $oid) {
                $order = wc_get_order($oid);
                if (!$order) { continue; }
                $found_pdf = false;
                foreach (AIEO_Fulfillment_Vouchers::get_for_order($oid) as $v) {
                    if ((string) $v['carrier'] !== $carrier_slug) { continue; }
                    if (!empty($v['label_pdf_path'])
                        && file_exists(trailingslashit($upload_dir['basedir']) . ltrim((string) $v['label_pdf_path'], '/'))) {
                        $pdf_urls[] = $base_url . ltrim((string) $v['label_pdf_path'], '/');
                        $found_pdf = true;
                        continue;
                    }
                    
                    $r = $driver->get_label_pdf((string) $v['voucher_no'], 'A4');
                    if (empty($r['ok']) || empty($r['pdf'])) { continue; }
                    $cached = $this->cache_label_pdf((int) $v['order_id'], (int) $v['id'], $carrier_slug, (string) $v['voucher_no'], (string) $r['pdf']);
                    
                    unset($r);
                    if ($cached) { $pdf_urls[] = $cached; $found_pdf = true; }
                }
                if (!$found_pdf) { $missing[] = (int) $oid; continue; }
                
                if (!in_array((string) $order->get_status(), $skip_status, true)) {
                    $order->update_status('completed', __('Bulk-generated voucher + printed + completed.', 'ai-eshop-optimizer'));
                    $completed_n++;
                }
                unset($order);
                if (function_exists('wp_cache_flush_runtime')) {
                    wp_cache_flush_runtime();
                }
            }

            if (empty($pdf_urls)) {
                return add_query_arg(array(
                    'aieo_ff_bulk_msg'      => 'no_labels_after_generate',
                    'aieo_ff_bulk_n'        => $created,
                    'aieo_ff_bulk_carrier'  => $carrier_slug,
                ), $redirect_to);
            }

            $token = wp_generate_password(20, false);
            set_transient('aieo_ff_a4x3_' . $token, array(
                'urls'        => $pdf_urls,
                'voucher_ids' => array_values(array_unique($print_voucher_ids)),
                'missing'     => $missing,
            ), 10 * MINUTE_IN_SECONDS);
            foreach ($print_voucher_ids as $vid) { AIEO_Fulfillment_Vouchers::mark_printed((int) $vid); }
            return admin_url('admin.php?aieo_ff_print_a4x3=' . $token);
        }

        
        if ($action === 'aieo_ff_print_labels_a4x3') {
            
            
            
            AIEO_Fulfillment_Bulk::raise_limits(300);
            $upload_dir = wp_upload_dir();
            $base_url   = trailingslashit($upload_dir['baseurl']);
            $pdf_urls   = array();
            $voucher_ids = array();
            $missing    = array();
            $couriers   = $this->couriers();
            foreach ($order_ids as $oid) {
                $vouchers = AIEO_Fulfillment_Vouchers::get_for_order($oid);

                
                
                
                
                
                
                if (empty($vouchers)) {
                    $order = wc_get_order($oid);
                    if ($order) {
                        $items = $order->get_meta('_wc_shipment_tracking_items');
                        if (is_array($items)) {
                            foreach ($items as $item) {
                                if (!is_array($item)) { continue; }
                                $tn = trim((string) ($item['tracking_number'] ?? ''));
                                if ($tn === '') { continue; }
                                $provider = trim((string) ($item['tracking_provider'] ?? ($item['custom_tracking_provider'] ?? '')));
                                
                                
                                
                                
                                $carrier = 'manual';
                                $pl = mb_strtolower($provider);
                                if (strpos($pl, 'geniki') !== false || strpos($pl, 'γενικ') !== false || strpos($pl, 'ταχυδρομικ') !== false || strpos($pl, 'taxydromik') !== false) {
                                    $carrier = 'geniki';
                                }
                                $existing = AIEO_Fulfillment_Vouchers::find_by_voucher_no($carrier, $tn);
                                if ($existing) { continue; }
                                $shipped_ts = (int) ($item['date_shipped'] ?? 0);
                                AIEO_Fulfillment_Vouchers::create(array(
                                    'order_id'        => (int) $oid,
                                    'carrier'         => $carrier,
                                    'voucher_no'      => $tn,
                                    'carrier_account' => $carrier === 'manual' ? $provider : '',
                                    'status'          => 'in_transit',
                                    'pieces'          => 1,
                                    'services_csv'    => '',
                                    'created_at'      => $shipped_ts > 0 ? gmdate('Y-m-d H:i:s', $shipped_ts) : current_time('mysql', true),
                                ));
                            }
                            $vouchers = AIEO_Fulfillment_Vouchers::get_for_order($oid);
                        }
                    }
                }

                $found = false;
                foreach ($vouchers as $v) {
                    
                    if (!empty($v['label_pdf_path'])) {
                        $abs = trailingslashit($upload_dir['basedir']) . ltrim((string) $v['label_pdf_path'], '/');
                        if (file_exists($abs)) {
                            $pdf_urls[]    = $base_url . ltrim((string) $v['label_pdf_path'], '/');
                            $voucher_ids[] = (int) $v['id'];
                            $found = true;
                            continue;
                        }
                    }
                    
                    
                    
                    $carrier = (string) $v['carrier'];
                    $driver  = isset($couriers[$carrier]) ? $couriers[$carrier] : null;
                    if (!$driver || !$driver->has_capability('print')) { continue; }
                    $r = $driver->get_label_pdf((string) $v['voucher_no'], 'A4');
                    if (empty($r['ok']) || empty($r['pdf'])) { continue; }
                    $cached = $this->cache_label_pdf((int) $v['order_id'], (int) $v['id'], $carrier, (string) $v['voucher_no'], (string) $r['pdf']);
                    
                    
                    
                    
                    unset($r);
                    if ($cached) {
                        $pdf_urls[]    = $cached;
                        $voucher_ids[] = (int) $v['id'];
                        $found = true;
                    }
                }
                if (!$found) { $missing[] = (int) $oid; }
                
                
                
                
                
                
                if (function_exists('wp_cache_flush_runtime')) {
                    wp_cache_flush_runtime();
                }
            }
            if (empty($pdf_urls)) {
                return add_query_arg(array('aieo_ff_bulk_msg' => 'no_labels', 'aieo_ff_bulk_n' => 0), $redirect_to);
            }
            
            
            
            $token = wp_generate_password(20, false);
            set_transient('aieo_ff_a4x3_' . $token, array(
                'urls'        => $pdf_urls,
                'voucher_ids' => $voucher_ids,
                'missing'     => $missing,
            ), 10 * MINUTE_IN_SECONDS);
            
            
            foreach ($voucher_ids as $vid) { AIEO_Fulfillment_Vouchers::mark_printed($vid); }
            return admin_url('admin.php?aieo_ff_print_a4x3=' . $token);
        }

        
        if ($action === 'aieo_ff_print_labels') {
            
            
            
            AIEO_Fulfillment_Bulk::raise_limits(300);
            $voucher_ids = array();
            foreach ($order_ids as $oid) {
                foreach (AIEO_Fulfillment_Vouchers::get_for_order($oid) as $v) {
                    if (!empty($v['label_pdf_path'])) { $voucher_ids[] = (int) $v['id']; }
                }
            }
            if (empty($voucher_ids)) {
                return add_query_arg(array('aieo_ff_bulk_msg' => 'no_labels', 'aieo_ff_bulk_n' => 0), $redirect_to);
            }
            $r = AIEO_Fulfillment_Bulk::zip_voucher_labels($voucher_ids);
            if (empty($r['ok']) || empty($r['zip_url'])) {
                return add_query_arg(array('aieo_ff_bulk_msg' => 'zip_failed', 'aieo_ff_bulk_err' => isset($r['error']) ? rawurlencode((string) $r['error']) : ''), $redirect_to);
            }
            
            foreach ($voucher_ids as $vid) {
                AIEO_Fulfillment_Vouchers::mark_printed((int) $vid);
            }
            return add_query_arg(array(
                'aieo_ff_bulk_msg' => 'zip_ready',
                'aieo_ff_bulk_n'   => (int) $r['count'],
                'aieo_ff_zip_url'  => rawurlencode((string) $r['zip_url']),
            ), $redirect_to);
        }

        
        $type = null;
        if ($action === 'aieo_ff_print_packing_slips') { $type = 'packing-slip'; }
        if ($action === 'aieo_ff_print_invoices')      { $type = 'invoice'; }

        if ($type !== null) {
            
            
            
            
            
            AIEO_Fulfillment_Bulk::raise_limits(600);
            $r = AIEO_Fulfillment_Bulk::merge_documents($order_ids, $type); 
            if (empty($r['ok']) || empty($r['pdf_url'])) {
                return add_query_arg(array(
                    'aieo_ff_bulk_msg' => 'merge_failed',
                    'aieo_ff_bulk_type' => $type,
                    'aieo_ff_bulk_err'  => isset($r['error']) ? rawurlencode((string) $r['error']) : '',
                ), $redirect_to);
            }
            
            
            
            foreach ($order_ids as $oid) {
                $existing = AIEO_Fulfillment_Documents::get_for_order_by_type((int) $oid, $type);
                if ($existing && !empty($existing['id'])) {
                    AIEO_Fulfillment_Documents::mark_printed((int) $existing['id']);
                }
            }
            return add_query_arg(array(
                'aieo_ff_bulk_msg'  => 'merge_ready',
                'aieo_ff_bulk_type' => $type,
                'aieo_ff_bulk_n'    => (int) ($r['merged'] ?? 0),
                'aieo_ff_pdf_url'   => rawurlencode((string) $r['pdf_url']),
            ), $redirect_to);
        }

        return $redirect_to;
    }

    
    public function orders_list_bulk_notice() {
        if (empty($_GET['aieo_ff_bulk_msg'])) { return; }
        $msg = sanitize_key((string) wp_unslash( $_GET['aieo_ff_bulk_msg'] ));
        $n   = isset($_GET['aieo_ff_bulk_n'])    ? (int) sanitize_text_field( wp_unslash( $_GET['aieo_ff_bulk_n'] ) ) : 0;
        $zip_url = isset($_GET['aieo_ff_zip_url']) ? esc_url_raw(urldecode((string) wp_unslash( $_GET['aieo_ff_zip_url'] ))) : '';
        $pdf_url = isset($_GET['aieo_ff_pdf_url']) ? esc_url_raw(urldecode((string) wp_unslash( $_GET['aieo_ff_pdf_url'] ))) : '';
        $type    = isset($_GET['aieo_ff_bulk_type']) ? sanitize_key((string) $_GET['aieo_ff_bulk_type']) : '';
        $err     = isset($_GET['aieo_ff_bulk_err'])  ? sanitize_text_field(urldecode((string) wp_unslash( $_GET['aieo_ff_bulk_err'] ))) : '';

        if ($msg === 'zip_ready' && $zip_url !== '') {
            
            echo '<div class="notice notice-success is-dismissible"><p>'
                
                . sprintf(esc_html__('Labels ZIP ready (%d files).', 'ai-eshop-optimizer'), $n) . ' ' 
                . '<a href="' . esc_url($zip_url) . '" class="button button-primary" download>' . esc_html__('Download ZIP', 'ai-eshop-optimizer') . '</a>'
                . '</p></div>';
            return;
        }

        if ($msg === 'merge_ready' && $pdf_url !== '') {
            $type_label = $type === 'packing-slip' ? __('packing slips', 'ai-eshop-optimizer')
                       : ($type === 'invoice'       ? __('invoices', 'ai-eshop-optimizer')
                                                    : __('documents', 'ai-eshop-optimizer'));
            
            echo '<div class="notice notice-success is-dismissible"><p>'
                
                . sprintf(esc_html__('Merged %1$d %2$s ready to print.', 'ai-eshop-optimizer'), $n, $type_label) . ' ' 
                . '<a href="' . esc_url($pdf_url) . '" class="button button-primary" target="_blank" rel="noopener" id="aieo-ff-bulk-pdf-link">' . esc_html__('Open PDF in new tab', 'ai-eshop-optimizer') . '</a>'
                . '</p></div>';
            
            
            
            
            ?>
            <script>(function(){ try { var w = window.open(<?php echo wp_json_encode($pdf_url); ?>, '_blank', 'noopener'); } catch(e){} })();</script>
            <?php
            return;
        }

        if ($msg === 'no_labels') {
            echo '<div class="notice notice-warning is-dismissible"><p>'
                . esc_html__('None of the selected orders have cached carrier labels yet. Generate the vouchers first (the API drivers will fetch labels automatically).', 'ai-eshop-optimizer')
                . '</p></div>';
            return;
        }

        if ($msg === 'zip_failed') {
            echo '<div class="notice notice-error is-dismissible"><p>'
                . esc_html__('Could not build the labels ZIP.', 'ai-eshop-optimizer')
                . ($err !== '' ? ' <code>' . esc_html($err) . '</code>' : '')
                . '</p></div>';
            return;
        }

        if ($msg === 'merge_failed') {
            echo '<div class="notice notice-error is-dismissible"><p>'
                . esc_html__('Could not build the merged PDF.', 'ai-eshop-optimizer')
                . ($err !== '' ? ' <code>' . esc_html($err) . '</code>' : '')
                . '</p></div>';
            return;
        }
    }

    public function ajax_close_pending_now() {
        $this->verify_admin_ajax();
        $couriers = $this->couriers();
        $results = array();
        foreach ($couriers as $code => $driver) {
            $has_close    = method_exists($driver, 'has_capability') && $driver->has_capability('close_pending');
            $has_manifest = method_exists($driver, 'has_capability') && $driver->has_capability('manifest');
            if (!$has_close && !$has_manifest) { continue; } 
            try {
                
                
                
                
                
                $r = class_exists('AIEO_Fulfillment_Closings')
                    ? AIEO_Fulfillment_Closings::close_and_log($driver, 'now')
                    : ($has_close ? $driver->close_pending() : array('ok' => false, 'error' => 'no closings helper'));
                $results[$code] = !empty($r['ok']) ? 'ok' : ('err: ' . (isset($r['error']) ? $r['error'] : 'unknown'));
            } catch (\Throwable $e) {
                $results[$code] = 'throw: ' . $e->getMessage();
            }
        }
        if (empty($results)) {
            wp_send_json_error(array('message' => __('No carrier has an end-of-day API close.', 'ai-eshop-optimizer')));
        }
        $msg = array();
        foreach ($results as $code => $r) { $msg[] = $code . '=' . $r; }
        wp_send_json_success(array('message' => implode(' · ', $msg)));
    }

    
    public function ajax_toggle_auto_close() {
        $this->verify_admin_ajax();
        $enabled = !empty($_POST['enabled']);
        AIEO_Fulfillment_Settings::set('auto_close_enabled', $enabled);
        wp_send_json_success(array(
            'enabled' => $enabled,
            'message' => $enabled
                ? __('Auto-close enabled. The daily cron reconciles every carrier — an end-of-day API close for those that have one (Geniki), a local dispatch manifest for those that don\'t (e.g. Courier Center).', 'ai-eshop-optimizer')
                : __('Auto-close disabled. You will need to close pending jobs manually each day.', 'ai-eshop-optimizer'),
        ));
    }

    
    public function ajax_close_pending_by_date() {
        $this->verify_admin_ajax();
        $date_from = isset($_POST['date_from']) ? sanitize_text_field((string) wp_unslash($_POST['date_from'])) : '';
        $date_to   = isset($_POST['date_to'])   ? sanitize_text_field((string) wp_unslash($_POST['date_to']))   : '';
        if ($date_from === '' || $date_to === '') {
            wp_send_json_error(array('message' => __('date_from and date_to are required.', 'ai-eshop-optimizer')));
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to)) {
            wp_send_json_error(array('message' => __('Dates must be YYYY-MM-DD.', 'ai-eshop-optimizer')));
        }
        if (strcmp($date_from, $date_to) > 0) {
            wp_send_json_error(array('message' => __('date_from must be on or before date_to.', 'ai-eshop-optimizer')));
        }

        $from_iso = $date_from . 'T00:00:00';
        $to_iso   = $date_to   . 'T23:59:59';

        $couriers = $this->couriers();
        $results = array();
        foreach ($couriers as $code => $driver) {
            if (!$driver->has_capability('close_pending_by_date')) { continue; }
            try {
                $r = $driver->close_pending_by_date($from_iso, $to_iso);
                $results[$code] = !empty($r['ok']) ? 'ok' : ('err: ' . (isset($r['error']) ? $r['error'] : 'unknown'));
            } catch (\Throwable $e) {
                $results[$code] = 'throw: ' . $e->getMessage();
            }
        }
        if (empty($results)) {
            wp_send_json_error(array('message' => __('No driver advertises close_pending_by_date.', 'ai-eshop-optimizer')));
        }
        $msg = array();
        foreach ($results as $code => $r) { $msg[] = $code . '=' . $r; }
        wp_send_json_success(array(
            
            'message' => sprintf(__('Closed range %1$s → %2$s · %3$s', 'ai-eshop-optimizer'), $date_from, $date_to, implode(' · ', $msg)),
            'date_from' => $date_from,
            'date_to'   => $date_to,
        ));
    }

    
    public function ajax_close_and_print() {
        $this->verify_admin_ajax();
        if (!class_exists('AIEO_Fulfillment_Closings')) {
            wp_send_json_error(array('message' => 'closings class unavailable'));
        }
        $carrier   = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : 'geniki';
        $mode      = isset($_POST['mode'])    ? sanitize_key((string) wp_unslash($_POST['mode']))    : 'now';
        $date_from = isset($_POST['date_from']) ? sanitize_text_field((string) wp_unslash($_POST['date_from'])) : null;
        $date_to   = isset($_POST['date_to'])   ? sanitize_text_field((string) wp_unslash($_POST['date_to']))   : null;
        if ($mode === 'by_date') {
            if (!$date_from || !$date_to
                || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from)
                || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to)
                || strcmp($date_from, $date_to) > 0) {
                wp_send_json_error(array('message' => __('Pick a valid date range (YYYY-MM-DD).', 'ai-eshop-optimizer')));
            }
        } else {
            $mode = 'now';
            $date_from = null;
            $date_to   = null;
        }
        $couriers = $this->couriers();
        
        
        
        
        if (empty($couriers[$carrier])) {
            
            wp_send_json_error(array('message' => sprintf(__('Unknown carrier: %s.', 'ai-eshop-optimizer'), $carrier)));
        }
        $r = AIEO_Fulfillment_Closings::close_and_log($couriers[$carrier], $mode, $date_from, $date_to);
        if (empty($r['ok']) || empty($r['id'])) {
            wp_send_json_error(array(
                'message' => sprintf(
                    
                    __('Carrier %1$s close failed: %2$s', 'ai-eshop-optimizer'),
                    $carrier,
                    isset($r['error']) ? $r['error'] : 'unknown'
                ),
            ));
        }
        $pdf_rel = AIEO_Fulfillment_Closings::ensure_pdf((int) $r['id']);
        if (is_wp_error($pdf_rel)) {
            wp_send_json_error(array(
                'message' => sprintf(
                    
                    __('Closed %s, but manifest PDF failed:', 'ai-eshop-optimizer'),
                    $carrier
                ) . ' ' . $pdf_rel->get_error_message(),
            ));
        }
        wp_send_json_success(array(
            'message' => sprintf(
                
                __('Closed %1$d voucher(s) for %2$s.', 'ai-eshop-optimizer'),
                (int) $r['voucher_count'],
                $carrier
            ),
            'closing_id' => (int) $r['id'],
            'pdf_url'    => $this->build_manifest_url((int) $r['id']),
            'voucher_count' => (int) $r['voucher_count'],
        ));
    }

    
    public function ajax_reprint_last_closing() {
        $this->verify_admin_ajax();
        if (!class_exists('AIEO_Fulfillment_Closings')) {
            wp_send_json_error(array('message' => 'closings class unavailable'));
        }
        $carrier = isset($_POST['carrier']) ? sanitize_key((string) wp_unslash($_POST['carrier'])) : 'geniki';
        $row = AIEO_Fulfillment_Closings::get_last_for_carrier($carrier);
        if (!$row) {
            wp_send_json_error(array(
                'message' => sprintf(
                    
                    __('No prior closing found for %s.', 'ai-eshop-optimizer'),
                    $carrier
                ),
            ));
        }
        $pdf_rel = AIEO_Fulfillment_Closings::ensure_pdf((int) $row['id']);
        if (is_wp_error($pdf_rel)) {
            wp_send_json_error(array('message' => $pdf_rel->get_error_message()));
        }
        wp_send_json_success(array(
            'message'    => sprintf(
                
                __('Reprinting last %1$s closing (%2$d vouchers, %3$s).', 'ai-eshop-optimizer'),
                $carrier,
                (int) $row['voucher_count'],
                (string) $row['closed_at']
            ),
            'closing_id' => (int) $row['id'],
            'pdf_url'    => $this->build_manifest_url((int) $row['id']),
        ));
    }

    
    public function ajax_download_manifest() {
        if (!current_user_can('manage_woocommerce')) {
            status_header(403);
            echo 'forbidden';
            exit;
        }
        $closing_id = isset($_GET['closing_id']) ? (int) $_GET['closing_id'] : 0; 
        if ($closing_id <= 0) {
            status_header(400);
            echo 'closing_id required';
            exit;
        }
        if (!class_exists('AIEO_Fulfillment_Closings')) {
            status_header(500);
            echo 'closings class unavailable';
            exit;
        }
        $pdf_rel = AIEO_Fulfillment_Closings::ensure_pdf($closing_id);
        if (is_wp_error($pdf_rel)) {
            status_header(500);
            echo esc_html($pdf_rel->get_error_message());
            exit;
        }
        $abs = AIEO_Fulfillment_Closings::abs_from_rel($pdf_rel);
        if (!file_exists($abs)) {
            status_header(404);
            echo 'manifest pdf missing';
            exit;
        }
        nocache_headers();
        header('Content-Type: application/pdf');
        header('Content-Disposition: inline; filename="' . basename($abs) . '"');
        header('Content-Length: ' . filesize($abs));
        readfile($abs);
        exit;
    }

    private function build_manifest_url($closing_id) {
        return add_query_arg(
            array(
                'action'     => 'aieo_ff_download_manifest',
                'closing_id' => (int) $closing_id,
            ),
            admin_url('admin-ajax.php')
        );
    }

    

    
    private function build_voucher_payload(WC_Order $order) {
        
        
        
        $customer_shipping_paid = (float) $order->get_shipping_total();
        return array(
            
            
            
            'order_id'  => $order->get_id(),
            'recipient' => array(
                'name'    => trim($order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name()) ?: trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()),
                'address' => trim($order->get_shipping_address_1() . ' ' . $order->get_shipping_address_2()) ?: trim($order->get_billing_address_1() . ' ' . $order->get_billing_address_2()),
                'city'    => $order->get_shipping_city() ?: $order->get_billing_city(),
                'zip'     => $order->get_shipping_postcode() ?: $order->get_billing_postcode(),
                'country' => $order->get_shipping_country() ?: $order->get_billing_country() ?: 'GR',
                'phone'   => $order->get_billing_phone(),
                'email'   => $order->get_billing_email(),
            ),
            'reference_1'            => (string) $order->get_order_number(),
            'cod_amount'             => 0, 
            'customer_shipping_paid' => $customer_shipping_paid,
        );
    }

    
    private function cache_label_pdf($order_id, $voucher_id, $carrier, $voucher_no, $pdf_bytes) {
        $upload_dir = wp_upload_dir(null, false);
        $base       = $upload_dir['basedir'] . '/aieo-dmm/fulfillment/labels/' . gmdate('Y/m');
        if (!wp_mkdir_p($base)) { return ''; }

        $filename = sanitize_file_name($carrier . '-' . $voucher_no . '.pdf');
        $abs      = $base . '/' . $filename;
        file_put_contents($abs, $pdf_bytes);

        $rel = 'aieo-dmm/fulfillment/labels/' . gmdate('Y/m') . '/' . $filename;
        AIEO_Fulfillment_Vouchers::update($voucher_id, array('label_pdf_path' => $rel));
        
        
        return $upload_dir['baseurl'] . '/' . $rel;
    }

    
    public function couriers() {
        $drivers = array();
        return apply_filters('aieo_fulfillment_couriers', $drivers);
    }

    
    public function active_couriers() {
        $out = array();
        foreach ($this->couriers() as $code => $driver) {
            $cfg = class_exists('AIEO_Fulfillment_Settings')
                ? (array) AIEO_Fulfillment_Settings::get('courier_' . $code, array())
                : array();
            if (empty($cfg)) { continue; }
            if (!method_exists($driver, 'has_capability') || !$driver->has_capability('create')) { continue; }
            $mode = method_exists($driver, 'create_mode') ? (string) $driver->create_mode() : 'api';
            $out[$code] = array('driver' => $driver, 'mode' => $mode);
        }
        return $out;
    }

    
    public function manual_entry_couriers() {
        $out = array();
        foreach ($this->couriers() as $code => $driver) {
            if (method_exists($driver, 'has_capability') && $driver->has_capability('create')) {
                $out[$code] = $driver;
            }
        }
        return $out;
    }

    
    public function has_api_courier_configured() {
        foreach ($this->active_couriers() as $a) {
            if ($a['mode'] === 'api') { return true; }
        }
        return false;
    }

    
    public function get_carrier_map() {
        $full = $this->get_carrier_map_full();
        $out  = array();
        foreach ($full as $title => $entry) {
            if (!empty($entry['carrier'])) { $out[$title] = $entry['carrier']; }
        }
        return $out;
    }

    
    public function get_carrier_map_full() {
        $raw = class_exists('AIEO_Fulfillment_Settings')
            ? AIEO_Fulfillment_Settings::get('shipping_method_carrier_map', array())
            : array();
        if (!is_array($raw)) { return array(); }

        
        $titles_by_id = array();
        $ids_by_title = array();
        foreach ($this->get_zone_shipping_methods(true) as $m) {
            $iid = (int) $m['instance_id'];
            $t   = trim((string) $m['title']);
            if ($iid > 0) { $titles_by_id[$iid] = $t; }
            if ($t !== '') { $ids_by_title[$t][] = $iid; }
        }

        $out          = array();
        $needs_resave = false;
        foreach ($raw as $title => $value) {
            
            
            if (is_int($title) || ctype_digit((string) $title)) { continue; }
            $title = trim((string) $title);
            if ($title === '') { continue; }

            
            
            if (!is_array($value)) {
                $code = sanitize_key((string) $value);
                if ($code === '') { continue; }
                $iid  = isset($ids_by_title[$title]) && !empty($ids_by_title[$title])
                    ? (int) $ids_by_title[$title][0] : 0;
                $value = array(
                    'carrier'     => $code,
                    'instance_id' => $iid,
                    'method_id'   => '',
                );
                $needs_resave = true;
            }

            $carrier     = sanitize_key((string) ($value['carrier'] ?? ''));
            $instance_id = (int) ($value['instance_id'] ?? 0);
            $method_id   = (string) ($value['method_id'] ?? '');
            if ($carrier === '') { continue; }

            
            
            
            $title_exists = isset($ids_by_title[$title]);
            $iid_exists   = $instance_id > 0 && isset($titles_by_id[$instance_id]);

            if (!$title_exists && $iid_exists) {
                
                
                
                $new_title = trim($titles_by_id[$instance_id]);
                if ($new_title !== '' && $new_title !== $title) {
                    $entry = $value;
                    $entry['renamed']       = true;
                    $entry['broken']        = false;
                    $entry['current_title'] = $new_title;
                    $out[$new_title] = $entry;
                    $needs_resave    = true;
                    continue;
                }
            }

            $value['renamed']       = false;
            $value['broken']        = !$title_exists && !$iid_exists;
            $value['current_title'] = $title_exists ? $title : ($iid_exists ? $titles_by_id[$instance_id] : '');
            $out[$title] = $value;
        }

        
        if ($needs_resave && class_exists('AIEO_Fulfillment_Settings')) {
            $persist = array();
            foreach ($out as $title => $entry) {
                $persist[$title] = array(
                    'carrier'     => (string) $entry['carrier'],
                    'instance_id' => (int) $entry['instance_id'],
                    'method_id'   => (string) $entry['method_id'],
                );
            }
            AIEO_Fulfillment_Settings::set('shipping_method_carrier_map', $persist);
        }

        return $out;
    }

    
    public function set_carrier_map_entry($title, $carrier_code) {
        if (!class_exists('AIEO_Fulfillment_Settings')) { return false; }
        $title = trim((string) $title);
        if ($title === '') { return false; }
        $code  = sanitize_key((string) $carrier_code);

        
        
        $raw = AIEO_Fulfillment_Settings::get('shipping_method_carrier_map', array());
        if (!is_array($raw)) { $raw = array(); }

        if ($code === '') {
            unset($raw[$title]);
            return (bool) AIEO_Fulfillment_Settings::set('shipping_method_carrier_map', $raw);
        }

        
        if (!isset($this->manual_entry_couriers()[$code])) { return false; }

        
        
        $instance_id = 0;
        $method_id   = '';
        foreach ($this->get_zone_shipping_methods(true) as $m) {
            if (trim((string) $m['title']) === $title) {
                $instance_id = (int) $m['instance_id'];
                $method_id   = (string) $m['method_id'];
                break;
            }
        }

        $raw[$title] = array(
            'carrier'     => $code,
            'instance_id' => $instance_id,
            'method_id'   => $method_id,
        );
        return (bool) AIEO_Fulfillment_Settings::set('shipping_method_carrier_map', $raw);
    }

    
    public function get_carrier_for_order($order_id_or_order) {
        $order = is_object($order_id_or_order) ? $order_id_or_order : (function_exists('wc_get_order') ? wc_get_order((int) $order_id_or_order) : null);
        if (!$order || !method_exists($order, 'get_shipping_methods')) { return ''; }
        $map = $this->get_carrier_map();
        if (empty($map)) { return ''; }
        foreach ($order->get_shipping_methods() as $item) {
            if (!is_object($item)) { continue; }
            $title = method_exists($item, 'get_method_title') ? (string) $item->get_method_title() : '';
            if ($title === '' && method_exists($item, 'get_name')) { $title = (string) $item->get_name(); }
            $title = trim($title);
            if ($title !== '' && isset($map[$title])) { return $map[$title]; }
        }
        return '';
    }

    
    public function get_distinct_shipping_titles() {
        $methods = $this->get_zone_shipping_methods(true); 
        $by_title = array();
        foreach ($methods as $m) {
            $t = trim((string) $m['title']);
            if ($t === '') { continue; }
            if (!isset($by_title[$t])) {
                $by_title[$t] = array(
                    'title'          => $t,
                    'zones'          => array(),
                    'enabled'        => false,
                    'disabled_count' => 0,
                );
            }
            $zone_name = (string) $m['zone_name'];
            if ($zone_name !== '' && !in_array($zone_name, $by_title[$t]['zones'], true)) {
                $by_title[$t]['zones'][] = $zone_name;
            }
            if (!empty($m['enabled'])) {
                $by_title[$t]['enabled'] = true;
            } else {
                $by_title[$t]['disabled_count']++;
            }
        }
        
        ksort($by_title, SORT_NATURAL | SORT_FLAG_CASE);
        return $by_title;
    }

    
    public function get_zone_shipping_methods($include_disabled = false) {
        if (!class_exists('WC_Shipping_Zones')) { return array(); }
        $out = array();
        
        $zones = WC_Shipping_Zones::get_zones();
        $rest  = new WC_Shipping_Zone(0);
        $rest_methods = $rest->get_shipping_methods(false);
        $rest_zone = array('zone_name' => __('Rest of the world', 'ai-eshop-optimizer'), 'shipping_methods' => $rest_methods);
        $all = $zones + array(0 => $rest_zone);
        foreach ($all as $zid => $zone) {
            $methods = isset($zone['shipping_methods']) ? $zone['shipping_methods'] : array();
            $zone_name = (string) ($zone['zone_name'] ?? '');
            foreach ($methods as $m) {
                if (!is_object($m)) { continue; }
                $enabled = method_exists($m, 'is_enabled') ? (bool) $m->is_enabled() : true;
                if (!$include_disabled && !$enabled) { continue; }
                $instance_id = isset($m->instance_id) ? (int) $m->instance_id : (method_exists($m, 'get_instance_id') ? (int) $m->get_instance_id() : 0);
                if ($instance_id <= 0) { continue; }
                $title = method_exists($m, 'get_title') ? (string) $m->get_title() : '';
                if ($title === '') { $title = isset($m->method_title) ? (string) $m->method_title : ''; }
                $out[] = array(
                    'instance_id' => $instance_id,
                    'method_id'   => isset($m->id) ? (string) $m->id : '',
                    'title'       => $title,
                    'zone_id'     => (int) $zid,
                    'zone_name'   => $zone_name,
                    'enabled'     => $enabled,
                );
            }
        }
        return $out;
    }

    
    public function linked_carriers() {
        $map = $this->get_carrier_map();
        if (empty($map)) { return $this->manual_entry_couriers(); }
        
        
        
        
        
        $titles_index = $this->get_distinct_shipping_titles();
        $live_codes = array();
        foreach ($map as $title => $code) {
            if (!isset($titles_index[$title])) { continue; }
            if (empty($titles_index[$title]['enabled'])) { continue; }
            $live_codes[(string) $code] = true;
        }
        $all = $this->manual_entry_couriers();
        $out = array();
        foreach ($live_codes as $code => $_) {
            if (isset($all[$code])) { $out[$code] = $all[$code]; }
        }
        
        
        
        
        
        return $out;
    }

    
    public function documents() {
        $docs = array();
        return apply_filters('aieo_fulfillment_documents', $docs);
    }

    public function render_subtab() {
        $section = isset($_GET['ff_section']) ? sanitize_key((string) $_GET['ff_section']) : 'couriers';
        
        
        
        if ($section === 'templates') { $section = 'documents'; }
        if (!in_array($section, array('couriers', 'documents', 'runlog', 'settings'), true)) {
            $section = 'couriers';
        }
        $base_url = admin_url('admin.php?page=' . AIEO_DMM::MENU_SLUG . '&dmm_tab=fulfillment');

        echo '<div class="aieo-dmm-section">';
        echo '<h2>' . esc_html__('Order Documents & Couriers', 'ai-eshop-optimizer') . '</h2>';
        echo '<p class="description">' . esc_html__('Unified fulfillment hub — courier vouchers (ACS, Geniki, BOX NOW, manual) plus order documents (invoice, packing slip, proforma, credit note). Replaces the legacy WPO PDF triplet, ACS plugin, and WC Shipment Tracking plugin. Customer-facing tracking + signed PDF downloads live in the AIEO Runtime plugin.', 'ai-eshop-optimizer') . '</p>';

        echo '<div class="nav-tab-wrapper" style="margin-bottom:14px;">';
        foreach (array(
            'couriers'  => __('Couriers', 'ai-eshop-optimizer'),
            'documents' => __('Documents', 'ai-eshop-optimizer'),
            'runlog'    => __('Delivery Statistics', 'ai-eshop-optimizer'),
            'settings'  => __('Settings', 'ai-eshop-optimizer'),
        ) as $slug => $label) {
            $active = $section === $slug ? ' nav-tab-active' : ''; 
            
            echo '<a href="' . esc_url($base_url . '&ff_section=' . $slug) . '" class="nav-tab' . $active . '">' . esc_html($label) . '</a>';
        }
        echo '</div>';

        switch ($section) {
            case 'couriers':  $this->render_couriers();  break;
            case 'documents': $this->render_documents(); break;
            case 'runlog':    $this->render_runlog();    break;
            case 'settings':  $this->render_settings();  break;
        }

        echo '</div>';
    }

    private function render_couriers() {
        $couriers = $this->couriers();
        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Registered carriers', 'ai-eshop-optimizer') . '</h3>';
        if (empty($couriers)) {
            echo '<p><em>' . esc_html__('No driver registered.', 'ai-eshop-optimizer') . '</em></p>';
            echo '</div>';
            return;
        }
        echo '<p class="description">' . esc_html__('Each carrier has its own settings card. Click "Test connection" after saving to verify credentials before going live.', 'ai-eshop-optimizer') . '</p>';
        echo '</div>';

        $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION);
        $ajax  = admin_url('admin-ajax.php');

        
        
        
        $this->render_custom_carriers_section();

        foreach ($couriers as $code => $driver) {
            if (!method_exists($driver, 'settings_schema')) {
                continue;
            }
            $schema = (array) $driver->settings_schema();
            
            
            
            
            if (!isset($schema['tracking_url_template'])) {
                $schema['tracking_url_template'] = array(
                    'type'  => 'url',
                    'label' => __('Tracking URL template (optional)', 'ai-eshop-optimizer'),
                    'help'  => __('Customer-facing tracking link emitted in order emails. Use {voucher_no} where the tracking number goes. Leave blank to use the carrier\'s built-in URL.', 'ai-eshop-optimizer'),
                );
            }
            $cfg    = class_exists('AIEO_Fulfillment_Settings')
                ? (array) AIEO_Fulfillment_Settings::get('courier_' . $code, array())
                : array();
            $caps   = method_exists($driver, 'capabilities') ? (array) $driver->capabilities() : array();
            $configured = !empty($cfg);
            ?>
            <div class="aieo-dmm-card aieo-courier-card" data-courier="<?php echo esc_attr($code); ?>" style="margin-top:18px;">
                <details<?php echo $configured ? '' : ' open'; ?>>
                    <summary style="cursor:pointer;font-size:15px;font-weight:600;padding:6px 0;">
                        <?php echo esc_html($driver->label()); ?>
                        <code style="margin-left:8px;font-weight:400;color:#646970;font-size:12px;"><?php echo esc_html($code); ?></code>
                        <?php if ($configured) : ?>
                            <span style="margin-left:8px;color:#15803d;font-weight:400;font-size:12px;">✓ <?php esc_html_e('configured', 'ai-eshop-optimizer'); ?></span>
                        <?php endif; ?>
                    </summary>

                    <p class="description" style="margin-top:8px;">
                        <strong><?php esc_html_e('Capabilities:', 'ai-eshop-optimizer'); ?></strong>
                        <?php echo esc_html(implode(', ', $caps) ?: '—'); ?>
                    </p>

                    <form class="aieo-courier-form" data-courier="<?php echo esc_attr($code); ?>">
                        <table class="form-table"><tbody>
                        <?php foreach ($schema as $field => $meta) :
                            $type     = (string) ($meta['type']     ?? 'text');
                            $label    = (string) ($meta['label']    ?? $field);
                            $help     = (string) ($meta['help']     ?? '');
                            $required = !empty($meta['required']);
                            $value    = $cfg[$field] ?? ($meta['default'] ?? '');
                        ?>
                            <tr>
                                <th><label for="aieo-courier-<?php echo esc_attr($code . '-' . $field); ?>">
                                    <?php echo esc_html($label); ?>
                                    <?php if ($required) : ?><span style="color:#b91c1c;">*</span><?php endif; ?>
                                </label></th>
                                <td>
                                <?php if ($type === 'checkbox') : ?>
                                    <label><input type="checkbox" id="aieo-courier-<?php echo esc_attr($code . '-' . $field); ?>" name="<?php echo esc_attr($field); ?>" value="1" <?php checked(!empty($value)); ?>> <?php echo esc_html($help ?: $label); ?></label>
                                <?php elseif ($type === 'number') : ?>
                                    <input type="number" id="aieo-courier-<?php echo esc_attr($code . '-' . $field); ?>" name="<?php echo esc_attr($field); ?>" value="<?php echo esc_attr((string) $value); ?>" step="any" style="width:160px;">
                                    <?php if ($help) : ?><p class="description"><?php echo esc_html($help); ?></p><?php endif; ?>
                                <?php elseif ($type === 'password') : ?>
                                    <input type="password" id="aieo-courier-<?php echo esc_attr($code . '-' . $field); ?>" name="<?php echo esc_attr($field); ?>" value="<?php echo esc_attr((string) $value); ?>" autocomplete="new-password" class="regular-text">
                                    <?php if ($help) : ?><p class="description"><?php echo esc_html($help); ?></p><?php endif; ?>
                                <?php elseif ($type === 'url') : ?>
                                    <input type="url" id="aieo-courier-<?php echo esc_attr($code . '-' . $field); ?>" name="<?php echo esc_attr($field); ?>" value="<?php echo esc_attr((string) $value); ?>" class="regular-text" placeholder="https://...">
                                    <?php if ($help) : ?><p class="description"><?php echo esc_html($help); ?></p><?php endif; ?>
                                <?php elseif ($type === 'select') : ?>
                                    <select id="aieo-courier-<?php echo esc_attr($code . '-' . $field); ?>" name="<?php echo esc_attr($field); ?>" class="regular-text">
                                        <option value=""<?php selected((string) $value, ''); ?>>— <?php esc_html_e('default', 'ai-eshop-optimizer'); ?> —</option>
                                        <?php foreach ((array) ($meta['options'] ?? array()) as $opt_val => $opt_label) : ?>
                                            <option value="<?php echo esc_attr((string) $opt_val); ?>" <?php selected((string) $value, (string) $opt_val); ?>><?php echo esc_html((string) $opt_label); ?></option>
                                        <?php endforeach; ?>
                                    </select>
                                    <?php if ($help) : ?><p class="description"><?php echo esc_html($help); ?></p><?php endif; ?>
                                <?php else :  ?>
                                    <input type="text" id="aieo-courier-<?php echo esc_attr($code . '-' . $field); ?>" name="<?php echo esc_attr($field); ?>" value="<?php echo esc_attr((string) $value); ?>" class="regular-text">
                                    <?php if ($help) : ?><p class="description"><?php echo esc_html($help); ?></p><?php endif; ?>
                                <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                        <?php
                        
                        
                        
                        
                        
                        
                        
                        
                        ?>
                        <tr>
                            <th><label for="aieo-courier-<?php echo esc_attr($code); ?>-linked-title"><?php esc_html_e('Linked WC shipping method', 'ai-eshop-optimizer'); ?></label></th>
                            <td>
                                <select id="aieo-courier-<?php echo esc_attr($code); ?>-linked-title" class="aieo-courier-link-title" data-carrier="<?php echo esc_attr($code); ?>" data-link-nonce="<?php echo esc_attr(wp_create_nonce('aieo_fulfillment_admin')); ?>">
                                    <?php echo $this->render_link_title_options($code);  ?>
                                </select>
                                <span class="aieo-courier-link-status" style="margin-left:8px;font-size:12px;color:#646970;"></span>
                                <p class="description"><?php esc_html_e('Saves on change. Methods already linked to another carrier are greyed out — pick one of those means choosing it would steal the link, so we block it.', 'ai-eshop-optimizer'); ?></p>
                            </td>
                        </tr>
                        </tbody></table>

                        <p>
                            <button type="button" class="button button-primary" data-action="save"><?php esc_html_e('Save settings', 'ai-eshop-optimizer'); ?></button>
                            <button type="button" class="button" data-action="test"><?php esc_html_e('Test connection', 'ai-eshop-optimizer'); ?></button>
                            <span class="aieo-courier-status" style="margin-left:10px;font-size:13px;color:#646970;"></span>
                        </p>
                    </form>
                </details>
            </div>
            <?php
        }
        ?>
        <script>
        (function(){
            var nonce = <?php echo wp_json_encode($nonce); ?>;
            var ajax  = <?php echo wp_json_encode($ajax); ?>;
            function post(action, payload, cb) {
                var fd = new FormData();
                fd.set('action', action);
                fd.set('nonce', nonce);
                Object.keys(payload).forEach(function(k){ fd.set(k, payload[k]); });
                fetch(ajax, { method:'POST', credentials:'same-origin', body: fd })
                    .then(function(r){ return r.json(); })
                    .then(cb)
                    .catch(function(){ cb && cb({success:false, data:{message:'network error'}}); });
            }
            document.addEventListener('click', function(ev){
                var btn = ev.target.closest('.aieo-courier-form button');
                if (!btn) { return; }
                var form = btn.closest('.aieo-courier-form');
                if (!form) { return; }
                var courier = form.dataset.courier;
                var status  = form.querySelector('.aieo-courier-status');
                var fd = new FormData(form);
                var fields = {};
                fd.forEach(function(v,k){ fields[k] = v; });
                // Checkboxes that are unchecked don't appear in FormData; ensure they're sent as ''.
                form.querySelectorAll('input[type="checkbox"]').forEach(function(cb){
                    if (!cb.checked) { fields[cb.name] = ''; }
                });
                var which = btn.dataset.action;
                status.style.color = '#646970';
                status.textContent = which === 'test'
                    ? '<?php echo esc_js(__('Testing…', 'ai-eshop-optimizer')); ?>'
                    : '<?php echo esc_js(__('Saving…', 'ai-eshop-optimizer')); ?>';
                post(which === 'test' ? 'aieo_ff_test_courier' : 'aieo_ff_save_courier',
                    { courier: courier, settings: JSON.stringify(fields) },
                    function(j){
                        if (j && j.success) {
                            status.style.color = '#15803d';
                            status.textContent = j.data.message || 'OK';
                        } else {
                            status.style.color = '#b91c1c';
                            status.textContent = (j && j.data && j.data.message) || 'error';
                        }
                    });
            });

            // Linked WC shipping-method dropdown — saves on change via
            // aieo_ff_save_carrier_map (uses its own nonce since it
            // verifies through verify_admin_ajax('aieo_fulfillment_admin')).
            document.addEventListener('change', function(ev){
                var sel = ev.target.closest && ev.target.closest('.aieo-courier-link-title');
                if (!sel) return;
                var status = sel.parentElement.querySelector('.aieo-courier-link-status');
                if (status) { status.style.color = '#6b7280'; status.textContent = '…'; }
                sel.disabled = true;
                var fd = new FormData();
                fd.set('action',  'aieo_ff_save_carrier_map');
                fd.set('nonce',   sel.dataset.linkNonce);
                fd.set('carrier', sel.dataset.carrier);
                fd.set('title',   sel.value);
                fetch(ajax, { method:'POST', credentials:'same-origin', body: fd })
                    .then(function(r){ return r.json(); })
                    .then(function(j){
                        sel.disabled = false;
                        if (!j || !j.success) {
                            if (status) { status.style.color = '#b91c1c'; status.textContent = '✗ ' + ((j && j.data && j.data.message) || 'fail'); }
                            return;
                        }
                        if (status) {
                            status.style.color = '#15803d';
                            status.textContent = '✓';
                            setTimeout(function(){ status.textContent = ''; }, 1800);
                        }
                    })
                    .catch(function(){
                        sel.disabled = false;
                        if (status) { status.style.color = '#b91c1c'; status.textContent = '✗ net'; }
                    });
            });
        })();
        </script>
        <?php
        
        
    }

    
    
    private function merge_carrier_options() {
        global $wpdb;
        $out = array();
        $prefix = class_exists('AIEO_Courier_Driver_Custom') ? AIEO_Courier_Driver_Custom::PREFIX : 'custom__';
        foreach ($this->couriers() as $slug => $driver) {
            $slug  = (string) $slug;
            $label = (is_object($driver) && method_exists($driver, 'label')) ? (string) $driver->label() : ucwords(str_replace(array('-', '_'), ' ', $slug));
            $out[$slug] = array('label' => $label, 'count' => 0, 'custom' => (strpos($slug, $prefix) === 0));
        }
        $vt   = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $rows = $wpdb->get_results("SELECT carrier, COUNT(*) c FROM {$vt} WHERE carrier <> '' GROUP BY carrier", ARRAY_A);
        foreach ((array) $rows as $r) {
            $slug = (string) $r['carrier'];
            if ($slug === '') { continue; }
            if (!isset($out[$slug])) {
                $out[$slug] = array('label' => ucwords(str_replace(array('-', '_'), ' ', $slug)), 'count' => 0, 'custom' => (strpos($slug, $prefix) === 0));
            }
            $out[$slug]['count'] = (int) $r['c'];
        }
        ksort($out);
        return $out;
    }

    
    private function render_merge_couriers_section() {
        $options = $this->merge_carrier_options();
        if (count($options) < 2) { return; }
        $ff_nonce  = wp_create_nonce('aieo_fulfillment_admin');
        $ajax      = admin_url('admin-ajax.php');
        $opts_html = '';
        foreach ($options as $slug => $meta) {
            
            $label = sprintf('%1$s — %2$s (%3$d %4$s)', $meta['label'], $slug, $meta['count'], _n('voucher', 'vouchers', $meta['count'], 'ai-eshop-optimizer'));
            $opts_html .= '<option value="' . esc_attr($slug) . '">' . esc_html($label) . '</option>';
        }
        ?>
        <div class="aieo-dmm-card" style="margin-top:18px;">
            <details open>
                <summary style="cursor:pointer;font-size:15px;font-weight:600;padding:6px 0;"><?php esc_html_e('Consolidate / merge couriers', 'ai-eshop-optimizer'); ?></summary>
                <p class="description" style="max-width:820px;">
                    <?php esc_html_e('Merge two carriers into one when the same courier ended up with duplicate entries (e.g. a built-in driver and a custom carrier). Everything keyed on the source carrier is re-pointed to the target: voucher history (your courier statistics), closing manifests, the shipping-method → carrier links, and the default carrier. If the source is a custom carrier it is then removed. Edits historical data — not reversible from the UI.', 'ai-eshop-optimizer'); ?>
                </p>
                <table class="form-table" role="presentation">
                    <tr>
                        <th scope="row"><label for="aieo-merge-from"><?php esc_html_e('Merge FROM (source — emptied)', 'ai-eshop-optimizer'); ?></label></th>
                        <td><select id="aieo-merge-from" style="min-width:360px;"><option value="">— —</option><?php echo $opts_html; ?></select></td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="aieo-merge-to"><?php esc_html_e('Merge INTO (target — keeps everything)', 'ai-eshop-optimizer'); ?></label></th>
                        <td><select id="aieo-merge-to" style="min-width:360px;"><option value="">— —</option><?php echo $opts_html; ?></select></td>
                    </tr>
                </table>
                <p>
                    <button type="button" class="button button-primary" id="aieo-merge-go" data-nonce="<?php echo esc_attr($ff_nonce); ?>"><?php esc_html_e('Merge couriers', 'ai-eshop-optimizer'); ?></button>
                    <span id="aieo-merge-status" style="margin-left:10px;font-size:13px;color:#646970;"></span>
                </p>
                <script>
                (function(){
                    var b=document.getElementById('aieo-merge-go'), st=document.getElementById('aieo-merge-status');
                    if(!b)return;
                    b.addEventListener('click',function(){
                        var from=document.getElementById('aieo-merge-from').value, to=document.getElementById('aieo-merge-to').value;
                        if(!from||!to){ st.style.color='#b91c1c'; st.textContent='<?php echo esc_js(__('Pick a source and a target.', 'ai-eshop-optimizer')); ?>'; return; }
                        if(from===to){ st.style.color='#b91c1c'; st.textContent='<?php echo esc_js(__('Source and target are identical.', 'ai-eshop-optimizer')); ?>'; return; }
                        if(!confirm('<?php echo esc_js(__('Merge', 'ai-eshop-optimizer')); ?> "'+from+'" → "'+to+'"?\n<?php echo esc_js(__('This re-points all voucher history + settings and edits historical data.', 'ai-eshop-optimizer')); ?>'))return;
                        b.disabled=true; st.style.color='#646970'; st.textContent='<?php echo esc_js(__('Merging…', 'ai-eshop-optimizer')); ?>';
                        var fd=new FormData(); fd.set('action','aieo_ff_merge_couriers'); fd.set('nonce',b.dataset.nonce); fd.set('from',from); fd.set('to',to);
                        fetch(<?php echo wp_json_encode($ajax); ?>,{method:'POST',credentials:'same-origin',body:fd}).then(function(r){return r.json();}).then(function(j){
                            b.disabled=false;
                            if(j&&j.success){ st.style.color='#15803d'; st.textContent=(j.data&&j.data.message)||'<?php echo esc_js(__('Done.', 'ai-eshop-optimizer')); ?>'; setTimeout(function(){location.reload();},1500); }
                            else{ st.style.color='#b91c1c'; st.textContent=(j&&j.data&&j.data.message)||'<?php echo esc_js(__('Failed.', 'ai-eshop-optimizer')); ?>'; }
                        }).catch(function(){ b.disabled=false; st.style.color='#b91c1c'; st.textContent='<?php echo esc_js(__('Failed.', 'ai-eshop-optimizer')); ?>'; });
                    });
                })();
                </script>
            </details>
        </div>
        <?php
    }

    
    public function ajax_merge_couriers() {
        $this->verify_admin_ajax();
        if (function_exists('set_time_limit')) { @set_time_limit(0); }
        global $wpdb;

        $from = isset($_POST['from']) ? sanitize_text_field(wp_unslash((string) $_POST['from'])) : '';
        $to   = isset($_POST['to'])   ? sanitize_text_field(wp_unslash((string) $_POST['to']))   : '';
        if ($from === '' || $to === '' || $from === $to) {
            wp_send_json_error(array('message' => __('Pick a source carrier and a different target carrier.', 'ai-eshop-optimizer')));
        }

        $vt = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $ct = $wpdb->prefix . 'aieo_dmm_fulfillment_closings';

        
        $vouchers = (int) $wpdb->query($wpdb->prepare("UPDATE {$vt} SET carrier = %s WHERE carrier = %s", $to, $from));
        
        $closings = (int) $wpdb->query($wpdb->prepare("UPDATE {$ct} SET carrier = %s WHERE carrier = %s", $to, $from));

        
        
        $map_moved = 0;
        if (class_exists('AIEO_Fulfillment_Settings')) {
            $raw = AIEO_Fulfillment_Settings::get('shipping_method_carrier_map', array());
            if (is_array($raw)) {
                $changed = false;
                foreach ($raw as $title => $entry) {
                    if (is_array($entry)) {
                        if (isset($entry['carrier']) && (string) $entry['carrier'] === $from) { $raw[$title]['carrier'] = $to; $map_moved++; $changed = true; }
                    } elseif ((string) $entry === $from) {
                        $raw[$title] = $to; $map_moved++; $changed = true;
                    }
                }
                if ($changed) { AIEO_Fulfillment_Settings::set('shipping_method_carrier_map', $raw); }
            }
            
            if ((string) AIEO_Fulfillment_Settings::get('default_carrier', '') === $from) {
                AIEO_Fulfillment_Settings::set('default_carrier', $to);
            }
        }

        
        $custom_removed = false;
        $prefix = class_exists('AIEO_Courier_Driver_Custom') ? AIEO_Courier_Driver_Custom::PREFIX : 'custom__';
        if (strpos($from, $prefix) === 0) {
            $custom_removed = (bool) $this->delete_custom_carrier(substr($from, strlen($prefix)));
        }

        $extra = $custom_removed ? ' · ' . __('source custom carrier removed', 'ai-eshop-optimizer') : '';
        wp_send_json_success(array(
            'vouchers'       => $vouchers,
            'closings'       => $closings,
            'map'            => $map_moved,
            'custom_removed' => $custom_removed,
            'message'        => sprintf(
                
                __('Merged "%1$s" → "%2$s". Vouchers moved: %3$d · closings: %4$d · method links: %5$d%6$s.', 'ai-eshop-optimizer'),
                $from, $to, $vouchers, $closings, $map_moved, $extra
            ),
        ));
    }

    private function render_custom_carriers_section() {
        $custom = $this->get_custom_carriers();
        $ff_nonce = wp_create_nonce('aieo_fulfillment_admin');
        $titles_idx = $this->get_distinct_shipping_titles();
        echo '<div class="aieo-dmm-card aieo-custom-carriers-card" style="margin-top:18px;">';
        echo '<details open><summary style="cursor:pointer;font-size:15px;font-weight:600;padding:6px 0;">' . esc_html__('Carriers without API (paste tracking number)', 'ai-eshop-optimizer') . '</summary>';
        echo '<p class="description">' . esc_html__('Add a carrier here when it has no API integration (e.g. Courier Center) — or when you simply prefer to paste tracking numbers manually for any other carrier. Each provider needs a label + a public tracking-URL template that uses {voucher_no} where the number goes (e.g. https://courier-center.gr/track?id={voucher_no}). Customers click this URL from their order notification email.', 'ai-eshop-optimizer') . '</p>';

        
        echo '<table class="widefat striped aieo-cc-sortable" style="margin:14px 0 12px;"><thead><tr>';
        echo '<th style="width:18%;cursor:pointer;user-select:none;" data-sort="label" title="' . esc_attr__('Click to sort', 'ai-eshop-optimizer') . '">' . esc_html__('Label', 'ai-eshop-optimizer') . ' <span class="aieo-cc-sort-icon" style="opacity:0.4;">⇅</span></th>';
        echo '<th style="width:12%;cursor:pointer;user-select:none;" data-sort="slug"  title="' . esc_attr__('Click to sort', 'ai-eshop-optimizer') . '">' . esc_html__('Slug', 'ai-eshop-optimizer') . ' <span class="aieo-cc-sort-icon" style="opacity:0.4;">⇅</span></th>';
        echo '<th style="width:36%;cursor:pointer;user-select:none;" data-sort="tpl"   title="' . esc_attr__('Click to sort', 'ai-eshop-optimizer') . '">' . esc_html__('Tracking URL template', 'ai-eshop-optimizer') . ' <span class="aieo-cc-sort-icon" style="opacity:0.4;">⇅</span></th>';
        echo '<th style="width:24%;cursor:pointer;user-select:none;" data-sort="link"  title="' . esc_attr__('Click to sort', 'ai-eshop-optimizer') . '">' . esc_html__('Linked WC shipping method', 'ai-eshop-optimizer') . ' <span class="aieo-cc-sort-icon" style="opacity:0.4;">⇅</span></th>';
        echo '<th style="width:10%;">' . esc_html__('Actions', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody class="aieo-cc-rows">';
        if (empty($custom)) {
            echo '<tr class="aieo-cc-empty"><td colspan="5"><em>' . esc_html__('No custom carriers yet — add one below.', 'ai-eshop-optimizer') . '</em></td></tr>';
        } else {
            foreach ($custom as $slug => $cfg) {
                $code = AIEO_Courier_Driver_Custom::PREFIX . $slug;
                $label_v = (string) $cfg['label'];
                $tpl_v   = (string) $cfg['tracking_url_template'];
                echo '<tr data-slug="' . esc_attr($slug) . '" data-sort-label="' . esc_attr(strtolower($label_v)) . '" data-sort-slug="' . esc_attr($slug) . '" data-sort-tpl="' . esc_attr(strtolower($tpl_v)) . '">';
                echo '<td class="aieo-cc-cell-label" data-original="' . esc_attr($label_v) . '">' . esc_html($label_v) . '</td>';
                echo '<td><code style="font-size:11px;">' . esc_html($slug) . '</code></td>';
                echo '<td class="aieo-cc-cell-tpl" data-original="' . esc_attr($tpl_v) . '"><code style="font-size:11px;word-break:break-all;">' . esc_html($tpl_v) . '</code></td>';
                echo '<td>';
                echo '<select class="aieo-courier-link-title" data-carrier="' . esc_attr($code) . '" data-link-nonce="' . esc_attr($ff_nonce) . '" style="width:100%;font-size:12px;">';
                
                echo $this->render_link_title_options($code);
                echo '</select>';
                echo '<span class="aieo-courier-link-status" style="font-size:11px;color:#646970;"></span>';
                echo '</td>';
                echo '<td class="aieo-cc-actions">';
                echo '<button type="button" class="button button-small aieo-cc-edit" data-slug="' . esc_attr($slug) . '" data-nonce="' . esc_attr($ff_nonce) . '">' . esc_html__('Edit', 'ai-eshop-optimizer') . '</button> ';
                echo '<button type="button" class="button button-small button-link-delete aieo-cc-delete" data-slug="' . esc_attr($slug) . '" data-nonce="' . esc_attr($ff_nonce) . '">' . esc_html__('Remove', 'ai-eshop-optimizer') . '</button>';
                echo '</td>';
                echo '</tr>';
            }
        }
        echo '</tbody></table>';

        
        echo '<div class="aieo-cc-add" style="background:#f6f7f7;border:1px solid #e2e4e7;border-radius:4px;padding:12px;">';
        echo '<h4 style="margin:0 0 8px;font-size:13px;">' . esc_html__('Add a custom carrier', 'ai-eshop-optimizer') . '</h4>';
        echo '<table class="form-table" style="margin:0;"><tbody>';
        echo '<tr><th style="width:160px;">' . esc_html__('Label', 'ai-eshop-optimizer') . ' <span style="color:#b91c1c;">*</span></th><td><input type="text" id="aieo-cc-new-label" class="regular-text" placeholder="' . esc_attr__('e.g. Courier Center', 'ai-eshop-optimizer') . '"></td></tr>';
        echo '<tr><th>' . esc_html__('Slug', 'ai-eshop-optimizer') . '</th><td><input type="text" id="aieo-cc-new-slug" class="regular-text" placeholder="' . esc_attr__('auto from label — letters / numbers / dashes', 'ai-eshop-optimizer') . '"><p class="description">' . esc_html__('Leave blank to auto-generate. Must be unique. Cannot be acs / geniki / boxnow / elta / manual / speedex.', 'ai-eshop-optimizer') . '</p></td></tr>';
        echo '<tr><th>' . esc_html__('Tracking URL template', 'ai-eshop-optimizer') . '</th><td><input type="url" id="aieo-cc-new-tpl" class="regular-text" placeholder="https://courier-center.gr/track?id={voucher_no}"><p class="description">' . esc_html__('Use {voucher_no} where the tracking number goes. Customers click this link from their order notification email.', 'ai-eshop-optimizer') . '</p></td></tr>';
        echo '<tr><th>' . esc_html__('Linked WC shipping method', 'ai-eshop-optimizer') . '</th><td><select id="aieo-cc-new-title" style="min-width:280px;">';
        
        
        
        echo $this->render_link_title_options('');
        echo '</select></td></tr>';
        echo '</tbody></table>';
        echo '<p style="margin:0;"><button type="button" class="button button-primary" id="aieo-cc-add-btn" data-nonce="' . esc_attr($ff_nonce) . '">' . esc_html__('Add carrier', 'ai-eshop-optimizer') . '</button> <span id="aieo-cc-add-status" style="margin-left:8px;font-size:12px;color:#646970;"></span></p>';
        echo '</div>';

        echo '</details></div>';

        $ajax = admin_url('admin-ajax.php');
        ?>
        <script>
        (function(){
            var ajax = <?php echo wp_json_encode($ajax); ?>;
            var addBtn = document.getElementById('aieo-cc-add-btn');
            var addStatus = document.getElementById('aieo-cc-add-status');
            if (addBtn) {
                addBtn.addEventListener('click', function(){
                    var label = document.getElementById('aieo-cc-new-label').value.trim();
                    var slug  = document.getElementById('aieo-cc-new-slug').value.trim();
                    var tpl   = document.getElementById('aieo-cc-new-tpl').value.trim();
                    var title = document.getElementById('aieo-cc-new-title').value;
                    addStatus.style.color = '#6b7280';
                    if (!label) { addStatus.style.color = '#b91c1c'; addStatus.textContent = '<?php echo esc_js(__('Label is required.', 'ai-eshop-optimizer')); ?>'; return; }
                    if (!slug)  { slug = label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, ''); }
                    addStatus.textContent = '<?php echo esc_js(__('Saving…', 'ai-eshop-optimizer')); ?>';
                    addBtn.disabled = true;
                    var fd = new FormData();
                    fd.set('action', 'aieo_ff_save_custom_carrier');
                    fd.set('nonce', addBtn.dataset.nonce);
                    fd.set('slug', slug);
                    fd.set('label', label);
                    fd.set('tracking_url_template', tpl);
                    fd.set('linked_method_title', title);
                    fetch(ajax, { method:'POST', credentials:'same-origin', body: fd })
                        .then(function(r){ return r.json(); })
                        .then(function(j){
                            addBtn.disabled = false;
                            if (!j || !j.success) {
                                addStatus.style.color = '#b91c1c';
                                addStatus.textContent = (j && j.data && j.data.message) || 'error';
                                return;
                            }
                            addStatus.style.color = '#15803d';
                            addStatus.textContent = '<?php echo esc_js(__('Saved — reloading…', 'ai-eshop-optimizer')); ?>';
                            setTimeout(function(){ location.reload(); }, 500);
                        })
                        .catch(function(){
                            addBtn.disabled = false;
                            addStatus.style.color = '#b91c1c';
                            addStatus.textContent = 'network error';
                        });
                });
            }
            document.querySelectorAll('.aieo-cc-delete').forEach(function(btn){
                btn.addEventListener('click', function(){
                    if (!confirm('<?php echo esc_js(__('Remove this custom carrier? Any shipping-method link will also be cleared.', 'ai-eshop-optimizer')); ?>')) return;
                    btn.disabled = true;
                    var fd = new FormData();
                    fd.set('action', 'aieo_ff_delete_custom_carrier');
                    fd.set('nonce', btn.dataset.nonce);
                    fd.set('slug', btn.dataset.slug);
                    fetch(ajax, { method:'POST', credentials:'same-origin', body: fd })
                        .then(function(r){ return r.json(); })
                        .then(function(j){
                            if (!j || !j.success) {
                                alert((j && j.data && j.data.message) || 'delete failed');
                                btn.disabled = false; return;
                            }
                            location.reload();
                        });
                });
            });

            // Sortable column headers — click cycles asc / desc / unsorted.
            // Sort keys are pre-baked onto each row's data-sort-* attributes
            // (lowercased) so we don't pay per-click DOM scrapes for label/
            // slug/tpl. The "link" column reads the dropdown's selected
            // option text at click time because that value can change after
            // an AJAX save without reload.
            (function(){
                var table = document.querySelector('table.aieo-cc-sortable');
                if (!table) return;
                var tbody = table.querySelector('tbody');
                var state = { key: null, dir: 0 };  // dir: 1=asc, -1=desc
                table.querySelectorAll('th[data-sort]').forEach(function(th){
                    th.addEventListener('click', function(){
                        var key = th.dataset.sort;
                        if (state.key === key) {
                            state.dir = state.dir === 1 ? -1 : (state.dir === -1 ? 0 : 1);
                        } else {
                            state.key = key; state.dir = 1;
                        }
                        // Reset icons.
                        table.querySelectorAll('.aieo-cc-sort-icon').forEach(function(s){ s.textContent = '⇅'; s.style.opacity = '0.4'; });
                        var icon = th.querySelector('.aieo-cc-sort-icon');
                        if (icon && state.dir !== 0) {
                            icon.textContent = state.dir === 1 ? '▲' : '▼';
                            icon.style.opacity = '1';
                        }
                        var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
                        if (state.dir === 0) {
                            // Restore original DOM order — fall back to data-slug to keep things deterministic.
                            rows.sort(function(a, b){
                                return (a.dataset.slug || '').localeCompare(b.dataset.slug || '');
                            });
                        } else {
                            var dir = state.dir;
                            rows.sort(function(a, b){
                                var va, vb;
                                if (key === 'link') {
                                    var sa = a.querySelector('select.aieo-courier-link-title');
                                    var sb = b.querySelector('select.aieo-courier-link-title');
                                    va = sa && sa.selectedIndex >= 0 ? sa.options[sa.selectedIndex].text.toLowerCase() : '';
                                    vb = sb && sb.selectedIndex >= 0 ? sb.options[sb.selectedIndex].text.toLowerCase() : '';
                                } else {
                                    va = a.dataset['sort' + key.charAt(0).toUpperCase() + key.slice(1)] || '';
                                    vb = b.dataset['sort' + key.charAt(0).toUpperCase() + key.slice(1)] || '';
                                }
                                return dir * va.localeCompare(vb, undefined, { numeric: true, sensitivity: 'base' });
                            });
                        }
                        rows.forEach(function(r){ tbody.appendChild(r); });
                    });
                });
            })();

            // Inline edit: turn the label + tracking-URL cells into inputs,
            // swap the action buttons for Save/Cancel, POST to the same
            // upsert endpoint (slug stays the key — saving with an existing
            // slug overwrites the entry).
            document.querySelectorAll('.aieo-cc-edit').forEach(function(btn){
                btn.addEventListener('click', function(){
                    var tr        = btn.closest('tr');
                    if (!tr) return;
                    var labelCell = tr.querySelector('.aieo-cc-cell-label');
                    var tplCell   = tr.querySelector('.aieo-cc-cell-tpl');
                    var actions   = tr.querySelector('.aieo-cc-actions');
                    if (!labelCell || !tplCell || !actions) return;

                    var origLabel = labelCell.dataset.original || labelCell.textContent.trim();
                    var origTpl   = tplCell.dataset.original  || tplCell.textContent.trim();

                    labelCell.innerHTML = '<input type="text" class="regular-text aieo-cc-edit-label" style="width:100%;font-size:12px;" value="' + origLabel.replace(/"/g, '&quot;') + '">';
                    tplCell.innerHTML   = '<input type="url"  class="regular-text aieo-cc-edit-tpl"   style="width:100%;font-size:11px;" value="' + origTpl.replace(/"/g, '&quot;')   + '">';
                    actions.innerHTML   = '<button type="button" class="button button-small button-primary aieo-cc-save">' + <?php echo wp_json_encode(__('Save', 'ai-eshop-optimizer')); ?> + '</button> ' +
                                          '<button type="button" class="button button-small aieo-cc-cancel">' + <?php echo wp_json_encode(__('Cancel', 'ai-eshop-optimizer')); ?> + '</button>' +
                                          '<span class="aieo-cc-edit-status" style="margin-left:8px;font-size:11px;color:#646970;"></span>';

                    var slug   = btn.dataset.slug;
                    var nonce  = btn.dataset.nonce;
                    var status = actions.querySelector('.aieo-cc-edit-status');

                    actions.querySelector('.aieo-cc-cancel').addEventListener('click', function(){
                        // Restore from data-original so we never trust the
                        // unsaved input values.
                        labelCell.innerHTML = origLabel.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
                        tplCell.innerHTML   = '<code style="font-size:11px;word-break:break-all;">' + origTpl.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</code>';
                        actions.innerHTML   = '<button type="button" class="button button-small aieo-cc-edit" data-slug="' + slug + '" data-nonce="' + nonce + '"><?php echo esc_js(__('Edit', 'ai-eshop-optimizer')); ?></button> ' +
                                              '<button type="button" class="button button-small button-link-delete aieo-cc-delete" data-slug="' + slug + '" data-nonce="' + nonce + '"><?php echo esc_js(__('Remove', 'ai-eshop-optimizer')); ?></button>';
                        // The replacement buttons aren't bound to listeners; a
                        // reload is the simplest way to rewire them.
                        // (No backend hit since Cancel was clicked.)
                        location.reload();
                    });

                    actions.querySelector('.aieo-cc-save').addEventListener('click', function(){
                        var newLabel = labelCell.querySelector('input').value.trim();
                        var newTpl   = tplCell.querySelector('input').value.trim();
                        if (!newLabel) {
                            status.style.color = '#b91c1c';
                            status.textContent = '<?php echo esc_js(__('Label is required.', 'ai-eshop-optimizer')); ?>';
                            return;
                        }
                        status.style.color = '#6b7280';
                        status.textContent = '<?php echo esc_js(__('Saving…', 'ai-eshop-optimizer')); ?>';
                        var fd = new FormData();
                        fd.set('action', 'aieo_ff_save_custom_carrier');
                        fd.set('nonce', nonce);
                        fd.set('slug', slug);
                        fd.set('label', newLabel);
                        fd.set('tracking_url_template', newTpl);
                        // linked_method_title intentionally omitted — Edit
                        // doesn't touch the shipping-method link (that has
                        // its own per-row dropdown that AJAX-saves on change).
                        fetch(ajax, { method:'POST', credentials:'same-origin', body: fd })
                            .then(function(r){ return r.json(); })
                            .then(function(j){
                                if (!j || !j.success) {
                                    status.style.color = '#b91c1c';
                                    status.textContent = (j && j.data && j.data.message) || 'save failed';
                                    return;
                                }
                                status.style.color = '#15803d';
                                status.textContent = '<?php echo esc_js(__('Saved — reloading…', 'ai-eshop-optimizer')); ?>';
                                setTimeout(function(){ location.reload(); }, 400);
                            })
                            .catch(function(){
                                status.style.color = '#b91c1c';
                                status.textContent = 'network error';
                            });
                    });
                });
            });
        })();
        </script>
        <?php
    }

    
    public function ajax_save_courier() {
        AIEO_DMM::verify_ajax_request();
        if (!class_exists('AIEO_Fulfillment_Settings')) {
            wp_send_json_error(array('message' => __('Fulfillment settings store unavailable.', 'ai-eshop-optimizer')));
        }
        $courier = isset($_POST['courier']) ? sanitize_key(wp_unslash($_POST['courier'])) : '';
        $couriers = $this->couriers();
        if ($courier === '' || empty($couriers[$courier])) {
            wp_send_json_error(array('message' => __('Unknown courier.', 'ai-eshop-optimizer')));
        }
        $driver = $couriers[$courier];
        $schema = method_exists($driver, 'settings_schema') ? (array) $driver->settings_schema() : array();
        
        
        if (!isset($schema['tracking_url_template'])) {
            $schema['tracking_url_template'] = array('type' => 'url');
        }

        $raw = isset($_POST['settings']) ? wp_unslash((string) sanitize_text_field( wp_unslash( $_POST['settings'] ) )) : '';
        $incoming = $raw !== '' ? json_decode($raw, true) : array();
        if (!is_array($incoming)) { $incoming = array(); }

        
        $clean = array();
        foreach ($schema as $field => $meta) {
            $type = (string) ($meta['type'] ?? 'text');
            $val  = $incoming[$field] ?? '';
            switch ($type) {
                case 'checkbox': $clean[$field] = !empty($val) ? 1 : 0; break;
                case 'number':   $clean[$field] = $val === '' ? '' : (float) $val; break;
                case 'password': 
                    $clean[$field] = (string) $val; break;
                case 'url':      $clean[$field] = esc_url_raw((string) $val); break;
                default:         $clean[$field] = sanitize_text_field((string) $val);
            }
        }
        AIEO_Fulfillment_Settings::set('courier_' . $courier, $clean);
        wp_send_json_success(array('message' => __('Saved.', 'ai-eshop-optimizer')));
    }

    
    public function ajax_test_courier() {
        AIEO_DMM::verify_ajax_request();
        $courier = isset($_POST['courier']) ? sanitize_key(wp_unslash($_POST['courier'])) : '';
        $couriers = $this->couriers();
        if ($courier === '' || empty($couriers[$courier])) {
            wp_send_json_error(array('message' => __('Unknown courier.', 'ai-eshop-optimizer')));
        }
        $driver = $couriers[$courier];
        if (!method_exists($driver, 'authenticate')) {
            wp_send_json_error(array('message' => __('Driver does not implement authenticate().', 'ai-eshop-optimizer')));
        }

        
        
        $raw = isset($_POST['settings']) ? wp_unslash((string) sanitize_text_field( wp_unslash( $_POST['settings'] ) )) : '';
        $overrides = $raw !== '' ? json_decode($raw, true) : array();
        if (!is_array($overrides)) { $overrides = array(); }

        $r = $driver->authenticate($overrides);
        if (is_array($r) && !empty($r['ok'])) {
            $cached = !empty($r['cached']) ? ' (cached)' : '';
            wp_send_json_success(array('message' => __('Authenticated successfully', 'ai-eshop-optimizer') . $cached));
        }
        $msg = is_array($r) && !empty($r['error']) ? $r['error'] : 'Authentication failed';
        wp_send_json_error(array('message' => $msg));
    }

    private function render_documents() {
        $docs  = $this->documents();
        $nonce = wp_create_nonce('aieo_fulfillment_admin');
        $today = current_time('Y-m-d');
        $week_ago = gmdate('Y-m-d', time() - 7 * DAY_IN_SECONDS);

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Registered document types', 'ai-eshop-optimizer') . '</h3>';
        if (empty($docs)) {
            echo '<p><em>' . esc_html__('No document type registered.', 'ai-eshop-optimizer') . '</em></p>';
        } else {
            echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
            echo '<th>' . esc_html__('Type', 'ai-eshop-optimizer') . '</th>';
            echo '<th>' . esc_html__('Label', 'ai-eshop-optimizer') . '</th>';
            echo '<th>' . esc_html__('Default email attachment', 'ai-eshop-optimizer') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($docs as $type => $doc) {
                echo '<tr>';
                echo '<td><code>' . esc_html($type) . '</code></td>';
                echo '<td>' . esc_html($doc->label()) . '</td>';
                echo '<td><code>' . esc_html(implode(', ', $doc->default_attached_to_emails()) ?: '—') . '</code></td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        }
        echo '</div>';

        
        echo '<div class="aieo-dmm-card" style="margin-top:18px;" data-aieo-ff-bulk data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('Bulk operations', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html__('Generate, merge, or ZIP documents for a date range of orders. Carrier label PDFs can also be ZIP-bundled.', 'ai-eshop-optimizer') . '</p>';

        echo '<table class="form-table"><tbody>';
        echo '<tr><th>' . esc_html__('Document type', 'ai-eshop-optimizer') . '</th><td><select id="aieo-ff-bulk-type">';
        foreach ($docs as $type => $doc) {
            echo '<option value="' . esc_attr($type) . '">' . esc_html($doc->label()) . '</option>';
        }
        echo '</select></td></tr>';
        echo '<tr><th>' . esc_html__('Date from', 'ai-eshop-optimizer') . '</th><td><input type="date" id="aieo-ff-bulk-from" value="' . esc_attr($week_ago) . '"></td></tr>';
        echo '<tr><th>' . esc_html__('Date to', 'ai-eshop-optimizer') . '</th><td><input type="date" id="aieo-ff-bulk-to"   value="' . esc_attr($today) . '"></td></tr>';
        echo '<tr><th>' . esc_html__('Order statuses', 'ai-eshop-optimizer') . '</th><td>';
        echo '<label><input type="checkbox" class="aieo-ff-bulk-status" value="wc-completed" checked> wc-completed</label> ';
        echo '<label><input type="checkbox" class="aieo-ff-bulk-status" value="wc-processing" checked> wc-processing</label> ';
        echo '<label><input type="checkbox" class="aieo-ff-bulk-status" value="wc-on-hold"> wc-on-hold</label>';
        echo '</td></tr>';
        echo '</tbody></table>';

        echo '<p>';
        echo '<button type="button" class="button button-primary" id="aieo-ff-bulk-merge">' . esc_html__('Merge → single PDF', 'ai-eshop-optimizer') . '</button> ';
        echo '<button type="button" class="button" id="aieo-ff-bulk-zip">' . esc_html__('Download ZIP', 'ai-eshop-optimizer') . '</button> ';
        echo '<span id="aieo-ff-bulk-status" style="margin-left:10px;font-size:13px;color:#646970;"></span>';
        echo '</p>';

        
        echo '<hr style="margin:18px 0;">';
        echo '<h4>' . esc_html__('Carrier labels', 'ai-eshop-optimizer') . '</h4>';
        echo '<p class="description">' . esc_html__('ZIP-bundle every cached carrier label (ACS, Geniki, Speedex partner) within a date range — operator workflow at end-of-day.', 'ai-eshop-optimizer') . '</p>';
        echo '<table class="form-table"><tbody>';
        echo '<tr><th>' . esc_html__('Carrier (optional filter)', 'ai-eshop-optimizer') . '</th><td><select id="aieo-ff-bulk-carrier"><option value="">' . esc_html__('All carriers', 'ai-eshop-optimizer') . '</option>';
        foreach ($this->couriers() as $code => $driver) {
            echo '<option value="' . esc_attr($code) . '">' . esc_html($driver->label()) . '</option>';
        }
        echo '</select></td></tr>';
        echo '<tr><th>' . esc_html__('Date from', 'ai-eshop-optimizer') . '</th><td><input type="date" id="aieo-ff-bulk-label-from" value="' . esc_attr($today) . '"></td></tr>';
        echo '<tr><th>' . esc_html__('Date to', 'ai-eshop-optimizer') . '</th><td><input type="date" id="aieo-ff-bulk-label-to"   value="' . esc_attr($today) . '"></td></tr>';
        echo '</tbody></table>';
        echo '<p><button type="button" class="button" id="aieo-ff-bulk-labels-zip">' . esc_html__('Download labels ZIP', 'ai-eshop-optimizer') . '</button> ';
        echo '<span id="aieo-ff-bulk-labels-status" style="margin-left:10px;font-size:13px;color:#646970;"></span></p>';

        
        echo '<hr style="margin:18px 0;">';
        echo '<h4>' . esc_html__('Daily carrier maintenance', 'ai-eshop-optimizer') . '</h4>';
        echo '<p class="description">' . esc_html__('End-of-day reconciliation. Carriers with a "close pending" API (e.g. Geniki Taxydromiki) report the day\'s vouchers to the carrier; carriers without one (e.g. Courier Center) get a local dispatch manifest and have the day\'s vouchers marked dispatched. The cron runs daily for every carrier; pick a carrier below to trigger it manually.', 'ai-eshop-optimizer') . '</p>';

        
        
        
        $auto_enabled = (bool) AIEO_Fulfillment_Settings::get('auto_close_enabled', true);
        $next_ts      = wp_next_scheduled('aieo_fulfillment_close_pending_tick');
        $last_run     = (string) AIEO_Fulfillment_Settings::get('auto_close_last_run_at', '');
        $last_results = (array)  AIEO_Fulfillment_Settings::get('auto_close_last_run_results', array());

        $next_human = $next_ts
            ? esc_html(mysql2date(get_option('date_format') . ' ' . get_option('time_format'), gmdate('Y-m-d H:i:s', (int) $next_ts)))
            : '<em>' . esc_html__('not scheduled', 'ai-eshop-optimizer') . '</em>';
        $last_human = $last_run !== ''
            ? esc_html(mysql2date(get_option('date_format') . ' ' . get_option('time_format'), $last_run))
            : '<em>' . esc_html__('never', 'ai-eshop-optimizer') . '</em>';
        $last_summary = '';
        if (!empty($last_results)) {
            $parts = array();
            foreach ($last_results as $code => $r) { $parts[] = esc_html($code . '=' . $r); }
            $last_summary = ' · ' . implode(' · ', $parts);
        }

        echo '<p style="background:#f1f5f9;padding:10px 12px;border-radius:6px;display:flex;align-items:center;flex-wrap:wrap;gap:14px;">';
        echo '<label style="margin:0;display:flex;align-items:center;gap:8px;cursor:pointer;">';
        echo '<input type="checkbox" id="aieo-ff-auto-close-toggle"' . ($auto_enabled ? ' checked' : '') . '>';
        echo '<strong>' . esc_html__('Auto-close enabled', 'ai-eshop-optimizer') . '</strong>';
        echo '</label>'; 
        echo '<span style="font-size:13px;color:#475569;">'; 
        
        echo esc_html__('Next run:', 'ai-eshop-optimizer') . ' <strong>' . $next_human . '</strong> · ';
        
        echo esc_html__('Last run:', 'ai-eshop-optimizer') . ' <strong>' . $last_human . '</strong>' . $last_summary;
        echo '</span>';
        echo '<span id="aieo-ff-auto-close-status" style="font-size:13px;color:#646970;margin-left:auto;"></span>';
        echo '</p>';

        
        
        
        
        
        $close_couriers = $this->couriers();
        $cc_default     = isset($close_couriers['geniki']) ? 'geniki' : (count($close_couriers) ? (string) array_key_first($close_couriers) : '');
        echo '<p style="margin:10px 0;">';
        echo '<select id="aieo-ff-close-carrier" style="min-width:220px;">';
        foreach ($close_couriers as $code => $driver) {
            $api_close = method_exists($driver, 'has_capability') && $driver->has_capability('close_pending');
            $suffix    = $api_close ? '' : ' — ' . esc_html__('manifest only', 'ai-eshop-optimizer');
            echo '<option value="' . esc_attr($code) . '"' . selected($cc_default, $code, false) . '>' . esc_html($driver->label()) . $suffix . '</option>';
        }
        echo '</select></p>';

        echo '<p><button type="button" class="button" id="aieo-ff-close-pending">' . esc_html__('Close pending jobs now (carrier API)', 'ai-eshop-optimizer') . '</button> ';
        echo '<button type="button" class="button button-primary" id="aieo-ff-close-and-print" style="margin-left:6px;">' . esc_html__('Close & print manifest', 'ai-eshop-optimizer') . '</button> ';
        echo '<button type="button" class="button" id="aieo-ff-reprint-last" style="margin-left:6px;">' . esc_html__('Reprint last closing', 'ai-eshop-optimizer') . '</button> ';
        echo '<span id="aieo-ff-close-status" style="margin-left:10px;font-size:13px;color:#646970;"></span></p>';
        echo '<p class="description" style="font-size:12px;margin-top:-4px;">' . esc_html__('"Close & print manifest" closes the selected carrier\'s pending vouchers for today AND opens the dispatcher PDF — for couriers with an end-of-day API (Geniki) it also reports the close to the carrier; for couriers without one (e.g. Courier Center) it produces the manifest and marks the vouchers dispatched locally. "Reprint last closing" re-renders the most recent successful manifest for the selected carrier.', 'ai-eshop-optimizer') . '</p>';

        
        
        
        
        
        $today_local = function_exists('wp_date') ? wp_date('Y-m-d') : gmdate('Y-m-d');
        echo '<p style="margin-top:14px;">';
        echo '<label style="margin-right:6px;"><strong>' . esc_html__('Close pending by date range:', 'ai-eshop-optimizer') . '</strong></label> ';
        echo '<label>' . esc_html__('From', 'ai-eshop-optimizer') . ' <input type="date" id="aieo-ff-close-from" value="' . esc_attr($today_local) . '" style="width:160px;"></label> ';
        echo '<label>' . esc_html__('To', 'ai-eshop-optimizer') . ' <input type="date" id="aieo-ff-close-to" value="' . esc_attr($today_local) . '" style="width:160px;"></label> ';
        echo '<button type="button" class="button" id="aieo-ff-close-pending-by-date">' . esc_html__('Close range', 'ai-eshop-optimizer') . '</button> ';
        echo '<span id="aieo-ff-close-by-date-status" style="margin-left:10px;font-size:13px;color:#646970;"></span></p>';
        echo '<p class="description" style="font-size:12px;">' . esc_html__('Use this to close vouchers from a specific past day (e.g. yesterday\'s jobs if today\'s pickup was missed). Ranges are inclusive — the end date is treated as 23:59:59.', 'ai-eshop-optimizer') . '</p>';

        echo '</div>'; 

        $this->print_documents_tab_js($nonce);
    }

    private function print_documents_tab_js($nonce) {
        $ajax = admin_url('admin-ajax.php');
        ?>
        <script>
        (function(){
            var nonce  = '<?php echo esc_js($nonce); ?>';
            var ajaxUrl = '<?php echo esc_js($ajax); ?>';
            function setStatus(id, color, text){ var el = document.getElementById(id); if (!el) return; el.style.color = color; el.textContent = text; }
            function post(action, data){
                var fd = new FormData();
                fd.set('action', action); fd.set('nonce', nonce);
                Object.keys(data || {}).forEach(function(k){ fd.set(k, data[k]); });
                return fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd}).then(function(r){return r.json();});
            }
            function bulkPayload() {
                var statuses = Array.prototype.map.call(document.querySelectorAll('.aieo-ff-bulk-status:checked'), function(cb){ return cb.value; }).join(',');
                return {
                    doc_type: (document.getElementById('aieo-ff-bulk-type') || {}).value || 'invoice',
                    date_from: (document.getElementById('aieo-ff-bulk-from') || {}).value || '',
                    date_to:   (document.getElementById('aieo-ff-bulk-to')   || {}).value || '',
                    statuses:  statuses
                };
            }

            var btn1 = document.getElementById('aieo-ff-bulk-merge');
            btn1 && btn1.addEventListener('click', function(){
                btn1.disabled = true;
                setStatus('aieo-ff-bulk-status', '#646970', '<?php echo esc_js(__('Building merged PDF…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_bulk_merge', bulkPayload()).then(function(j){
                    btn1.disabled = false;
                    if (j && j.success && j.data && j.data.pdf_url) {
                        window.open(j.data.pdf_url, '_blank', 'noopener');
                        setStatus('aieo-ff-bulk-status', '#15803d', (j.data.merged|0) + ' ' + '<?php echo esc_js(__('docs merged.', 'ai-eshop-optimizer')); ?>');
                    } else {
                        setStatus('aieo-ff-bulk-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Merge failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });
            var btn2 = document.getElementById('aieo-ff-bulk-zip');
            btn2 && btn2.addEventListener('click', function(){
                btn2.disabled = true;
                setStatus('aieo-ff-bulk-status', '#646970', '<?php echo esc_js(__('Building ZIP…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_bulk_zip', bulkPayload()).then(function(j){
                    btn2.disabled = false;
                    if (j && j.success && j.data && j.data.zip_url) {
                        window.location = j.data.zip_url;
                        setStatus('aieo-ff-bulk-status', '#15803d', (j.data.count|0) + ' ' + '<?php echo esc_js(__('PDFs zipped.', 'ai-eshop-optimizer')); ?>');
                    } else {
                        setStatus('aieo-ff-bulk-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('ZIP failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });

            var btn3 = document.getElementById('aieo-ff-bulk-labels-zip');
            btn3 && btn3.addEventListener('click', function(){
                btn3.disabled = true;
                setStatus('aieo-ff-bulk-labels-status', '#646970', '<?php echo esc_js(__('Building labels ZIP…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_bulk_zip_labels', {
                    carrier:    (document.getElementById('aieo-ff-bulk-carrier')||{}).value || '',
                    date_from:  (document.getElementById('aieo-ff-bulk-label-from')||{}).value || '',
                    date_to:    (document.getElementById('aieo-ff-bulk-label-to')||{}).value   || ''
                }).then(function(j){
                    btn3.disabled = false;
                    if (j && j.success && j.data && j.data.zip_url) {
                        window.location = j.data.zip_url;
                        setStatus('aieo-ff-bulk-labels-status', '#15803d', (j.data.count|0) + ' ' + '<?php echo esc_js(__('labels zipped.', 'ai-eshop-optimizer')); ?>');
                    } else {
                        setStatus('aieo-ff-bulk-labels-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('ZIP failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });

            var btn4 = document.getElementById('aieo-ff-close-pending');
            btn4 && btn4.addEventListener('click', function(){
                btn4.disabled = true;
                setStatus('aieo-ff-close-status', '#646970', '<?php echo esc_js(__('Calling Geniki…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_close_pending_now', {}).then(function(j){
                    btn4.disabled = false;
                    if (j && j.success) {
                        setStatus('aieo-ff-close-status', '#15803d', (j.data && j.data.message) || '<?php echo esc_js(__('Closed.', 'ai-eshop-optimizer')); ?>');
                    } else {
                        setStatus('aieo-ff-close-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Close failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });

            var toggle = document.getElementById('aieo-ff-auto-close-toggle');
            toggle && toggle.addEventListener('change', function(){
                var enabled = toggle.checked ? 1 : 0;
                setStatus('aieo-ff-auto-close-status', '#646970', '<?php echo esc_js(__('Saving…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_toggle_auto_close', {enabled: enabled}).then(function(j){
                    if (j && j.success) {
                        setStatus('aieo-ff-auto-close-status', '#15803d', (j.data && j.data.message) || '');
                    } else {
                        // Revert the checkbox if the save failed.
                        toggle.checked = !toggle.checked;
                        setStatus('aieo-ff-auto-close-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Save failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });

            // Close pending AND open the manifest PDF in a new tab.
            // Uses mode='now' so all of today's pending vouchers go in
            // a single manifest, matching the existing close-now button.
            var btnCp = document.getElementById('aieo-ff-close-and-print');
            btnCp && btnCp.addEventListener('click', function(){
                btnCp.disabled = true;
                setStatus('aieo-ff-close-status', '#646970', '<?php echo esc_js(__('Closing & rendering manifest…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_close_and_print', {carrier: (document.getElementById('aieo-ff-close-carrier')||{}).value || 'geniki', mode: 'now'}).then(function(j){
                    btnCp.disabled = false;
                    if (j && j.success && j.data && j.data.pdf_url) {
                        setStatus('aieo-ff-close-status', '#15803d', j.data.message || '');
                        window.open(j.data.pdf_url, '_blank', 'noopener');
                    } else {
                        setStatus('aieo-ff-close-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Close-and-print failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });

            // Reprint the most recent successful manifest — does NOT
            // touch the carrier; only re-renders the cached row.
            var btnRp = document.getElementById('aieo-ff-reprint-last');
            btnRp && btnRp.addEventListener('click', function(){
                btnRp.disabled = true;
                setStatus('aieo-ff-close-status', '#646970', '<?php echo esc_js(__('Loading last manifest…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_reprint_last_closing', {carrier: (document.getElementById('aieo-ff-close-carrier')||{}).value || 'geniki'}).then(function(j){
                    btnRp.disabled = false;
                    if (j && j.success && j.data && j.data.pdf_url) {
                        setStatus('aieo-ff-close-status', '#15803d', j.data.message || '');
                        window.open(j.data.pdf_url, '_blank', 'noopener');
                    } else {
                        setStatus('aieo-ff-close-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('No prior closing.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });

            var btn5 = document.getElementById('aieo-ff-close-pending-by-date');
            btn5 && btn5.addEventListener('click', function(){
                var from = (document.getElementById('aieo-ff-close-from')||{}).value || '';
                var to   = (document.getElementById('aieo-ff-close-to')  ||{}).value || '';
                if (!from || !to) {
                    setStatus('aieo-ff-close-by-date-status', '#b91c1c', '<?php echo esc_js(__('Pick both dates.', 'ai-eshop-optimizer')); ?>');
                    return;
                }
                btn5.disabled = true;
                setStatus('aieo-ff-close-by-date-status', '#646970', '<?php echo esc_js(__('Calling Geniki…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_close_pending_by_date', {date_from: from, date_to: to}).then(function(j){
                    btn5.disabled = false;
                    if (j && j.success) {
                        setStatus('aieo-ff-close-by-date-status', '#15803d', (j.data && j.data.message) || '<?php echo esc_js(__('Closed range.', 'ai-eshop-optimizer')); ?>');
                    } else {
                        setStatus('aieo-ff-close-by-date-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Close-by-date failed.', 'ai-eshop-optimizer')); ?>');
                    }
                });
            });
        })();
        </script>
        <?php
        
        $this->render_templates();
    }

    private function render_templates() {
        $tpl_list = class_exists('AIEO_Fulfillment_Templates') ? AIEO_Fulfillment_Templates::list_installed() : array();
        $active   = class_exists('AIEO_Fulfillment_Templates') ? AIEO_Fulfillment_Templates::active_template_id() : 'Simple';

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Document templates', 'ai-eshop-optimizer') . '</h3>';
        if (empty($tpl_list)) {
            echo '<p><em>' . esc_html__('No bundled templates.', 'ai-eshop-optimizer') . '</em></p>';
        } else {
            echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
            echo '<th>' . esc_html__('Template', 'ai-eshop-optimizer') . '</th>';
            echo '<th>' . esc_html__('Active', 'ai-eshop-optimizer') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($tpl_list as $id => $label) {
                echo '<tr>';
                echo '<td><code>' . esc_html($id) . '</code> — ' . esc_html($label) . '</td>';
                echo '<td>' . ($active === $id ? '<span style="color:#15803d;">✓</span>' : '—') . '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
            echo '<p class="description">' . esc_html__('Active template is changed via Settings → templates.active. Theme override: drop a copy at {theme}/aieo/fulfillment/{template}/{doc}.php.', 'ai-eshop-optimizer') . '</p>';
        }
        echo '</div>';
    }

    
    private function render_runlog() {
        $this->render_delivery_metrics_card();
    }

    
    private function render_activity_counters_card() {
        global $wpdb;
        $vt = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $ct = $wpdb->prefix . 'aieo_dmm_fulfillment_checkpoints';
        $dt = $wpdb->prefix . 'aieo_dmm_fulfillment_documents';

        $voucher_count    = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$vt}");
        $checkpoint_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$ct}");
        $document_count   = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$dt}");
        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Activity counters', 'ai-eshop-optimizer') . '</h3>'; 
        echo '<ul>';
        
        echo '<li>' . sprintf(esc_html__('Vouchers: %d', 'ai-eshop-optimizer'), $voucher_count) . '</li>'; 
        
        echo '<li>' . sprintf(esc_html__('Checkpoints: %d', 'ai-eshop-optimizer'), $checkpoint_count) . '</li>'; 
        
        echo '<li>' . sprintf(esc_html__('Documents: %d', 'ai-eshop-optimizer'), $document_count) . '</li>'; 
        echo '</ul>';
        echo '</div>';
    }

    
    private static function parse_locale_decimal($raw, $fallback = 0.0) {
        $s = preg_replace('/\s+/', '', (string) $raw);
        if ($s === '') { return (float) $fallback; }
        $last_comma = strrpos($s, ',');
        $last_dot   = strrpos($s, '.');
        if ($last_comma !== false && ($last_dot === false || $last_comma > $last_dot)) {
            $s = str_replace('.', '', $s);
            $s = str_replace(',', '.', $s);
        } elseif ($last_dot !== false) {
            $s = str_replace(',', '', $s);
        }
        return is_numeric($s) ? (float) $s : (float) $fallback;
    }

    
    private static function delivery_metrics_sanity_where() {
        return "AND v.status = 'delivered'
                AND v.delivered_at > v.created_at
                AND v.delivered_at > o.date_created_gmt
                AND v.delivered_at < (NOW() + INTERVAL 1 DAY)";
    }

    
    private function render_delivery_metrics_card() {
        global $wpdb;
        $vt = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $ot = $wpdb->prefix . 'wc_orders';
        $at = $wpdb->prefix . 'wc_order_addresses';

        
        
        $filter_carrier = isset($_GET['ff_metrics_carrier']) ? sanitize_key((string) wp_unslash($_GET['ff_metrics_carrier'])) : '';

        
        
        
        $prefix_len = (int) AIEO_Fulfillment_Settings::get('metrics_postcode_prefix_length', 2);
        if ($prefix_len < 1 || $prefix_len > 5) { $prefix_len = 2; }

        
        
        
        $margin       = AIEO_DMM::get_gross_margin();      
        $margin_pct   = AIEO_DMM::get_gross_margin_pct();  
        $fs_threshold = AIEO_DMM::get_free_shipping_threshold(); 

        echo '<div class="aieo-dmm-card" style="margin-top:18px;">';
        echo '<h3>' . esc_html__('Delivery time metrics by postal area', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . sprintf(
            
            esc_html__('Per-postal-area averages computed across delivered vouchers. Group key = first %d digit(s) of the shipping postcode after stripping spaces. Configurable under Settings → General.', 'ai-eshop-optimizer'),
            (int) $prefix_len
        ) . '</p>';

        
        $hpos_orders   = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $ot));
        $hpos_addr     = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $at));
        if (!$hpos_orders || !$hpos_addr) {
            echo '<p><em>' . esc_html__('Requires WooCommerce HPOS (high-performance order storage). Activate it under WooCommerce → Settings → Advanced → Features.', 'ai-eshop-optimizer') . '</em></p>';
            echo '</div>';
            return;
        }

        
        $couriers = $this->couriers();
        $base_url = admin_url('admin.php?page=' . AIEO_DMM::MENU_SLUG . '&dmm_tab=fulfillment&ff_section=runlog');
        
        $current_metrics_basis = isset($_GET['metrics_basis']) && sanitize_key((string) wp_unslash($_GET['metrics_basis'])) === 'fulfillment' ? 'fulfillment' : 'total';
        echo '<form method="get" style="margin-bottom:10px;">';
        echo '<input type="hidden" name="page" value="' . esc_attr(AIEO_DMM::MENU_SLUG) . '">';
        echo '<input type="hidden" name="dmm_tab" value="fulfillment">';
        echo '<input type="hidden" name="ff_section" value="runlog">';
        
        
        echo '<input type="hidden" name="metrics_basis" value="' . esc_attr($current_metrics_basis) . '">';
        echo '<label style="margin-right:6px;"><strong>' . esc_html__('Carrier:', 'ai-eshop-optimizer') . '</strong></label>';
        echo '<select name="ff_metrics_carrier">';
        echo '<option value="">' . esc_html__('All carriers', 'ai-eshop-optimizer') . '</option>';
        foreach ($couriers as $code => $driver) {
            echo '<option value="' . esc_attr($code) . '" ' . selected($filter_carrier, $code, false) . '>' . esc_html($driver->label()) . '</option>';
        }
        echo '</select> ';
        echo '<button type="submit" class="button button-small">' . esc_html__('Apply', 'ai-eshop-optimizer') . '</button>';
        if ($filter_carrier !== '') {
            echo ' <a href="' . esc_url($base_url) . '" class="button button-small">' . esc_html__('Clear', 'ai-eshop-optimizer') . '</a>';
        }
        echo '</form>';

        
        
        
        $export_nonce = wp_create_nonce('aieo_fulfillment_admin');
        echo '<form id="aieo-ff-metrics-export-form" method="POST" action="' . esc_url(admin_url('admin-ajax.php')) . '" style="display:inline-block;margin-bottom:10px;">';
        echo '<input type="hidden" name="action" value="aieo_ff_metrics_export_aggregate">';
        echo '<input type="hidden" name="nonce" value="' . esc_attr($export_nonce) . '">';
        echo '<input type="hidden" name="carrier" value="' . esc_attr($filter_carrier) . '">';
        echo '<button type="submit" class="button button-small">' . esc_html__('Export XLSX', 'ai-eshop-optimizer') . '</button>';
        echo '<span class="description" style="margin-left:8px;font-size:12px;">' . esc_html__('Same rows + totals as below, with the active carrier filter applied.', 'ai-eshop-optimizer') . '</span>';
        echo '</form>';

        
        
        
        $where_carrier = '';
        $params = array();
        if ($filter_carrier !== '') {
            $where_carrier = 'AND v.carrier = %s';
            $params[] = $filter_carrier;
        }

        
        
        
        
        
        $select_metrics = "
                AVG(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.created_at))   AS avg_hours_order_to_voucher,
                MIN(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.created_at))   AS min_hours_order_to_voucher,
                MAX(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.created_at))   AS max_hours_order_to_voucher,
                AVG(TIMESTAMPDIFF(HOUR, v.created_at, v.delivered_at))       AS avg_hours_voucher_to_delivery,
                MIN(TIMESTAMPDIFF(HOUR, v.created_at, v.delivered_at))       AS min_hours_voucher_to_delivery,
                MAX(TIMESTAMPDIFF(HOUR, v.created_at, v.delivered_at))       AS max_hours_voucher_to_delivery,
                AVG(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at)) AS avg_hours_order_to_delivery,
                MIN(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at)) AS min_hours_order_to_delivery,
                MAX(TIMESTAMPDIFF(HOUR, o.date_created_gmt, v.delivered_at)) AS max_hours_order_to_delivery,
                AVG(v.customer_shipping_paid)                                AS avg_customer_paid,
                SUM(v.customer_shipping_paid)                                AS sum_customer_paid,
                AVG(v.carrier_cost)                                          AS avg_carrier_cost,
                SUM(v.carrier_cost)                                          AS sum_carrier_cost,
                AVG(CASE WHEN v.carrier_cost IS NOT NULL
                         THEN v.customer_shipping_paid - v.carrier_cost
                         ELSE NULL END)                                      AS avg_net,
                SUM(CASE WHEN v.carrier_cost IS NOT NULL
                         THEN v.customer_shipping_paid - v.carrier_cost
                         ELSE 0 END)                                         AS sum_net,
                SUM(CASE WHEN v.carrier_cost IS NOT NULL THEN 1 ELSE 0 END)  AS shipments_with_cost,
                AVG(GREATEST(o.total_amount - COALESCE(v.customer_shipping_paid, 0), 0)) AS avg_net_products
        ";
        $sanity_where = self::delivery_metrics_sanity_where();
        $from_join = "
            FROM {$vt} v
            INNER JOIN {$ot} o ON o.id = v.order_id
            INNER JOIN {$at} a ON a.order_id = v.order_id AND a.address_type = 'shipping'
            WHERE v.delivered_at IS NOT NULL
              AND v.created_at   IS NOT NULL
              AND a.postcode     IS NOT NULL
              AND a.postcode     <> ''
              {$sanity_where}
              {$where_carrier}
        ";

        
        $sql = "
            SELECT
                LEFT(REPLACE(CAST(a.postcode AS CHAR), ' ', ''), {$prefix_len}) AS prefix,
                COUNT(*) AS shipments,
                {$select_metrics}
            {$from_join}
            GROUP BY prefix
            HAVING shipments > 0
            ORDER BY shipments DESC, prefix ASC
        ";
        $rows = empty($params) ? $wpdb->get_results($sql, ARRAY_A) : $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);

        $totals_sql = "SELECT COUNT(*) AS shipments, {$select_metrics} {$from_join}";
        $totals = empty($params) ? $wpdb->get_row($totals_sql, ARRAY_A) : $wpdb->get_row($wpdb->prepare($totals_sql, $params), ARRAY_A);

        
        
        
        
        
        
        
        $metrics_basis = isset($_GET['metrics_basis']) && sanitize_key((string) wp_unslash($_GET['metrics_basis'])) === 'fulfillment'
            ? 'fulfillment' : 'total';
        
        
        
        $end_col   = $metrics_basis === 'fulfillment' ? 'v.arrived_at' : 'v.delivered_at';
        $end_where = $metrics_basis === 'fulfillment'
            ? 'v.arrived_at IS NOT NULL'
            : 'v.delivered_at IS NOT NULL';

        
        
        $dist_sql = "
            SELECT
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <=  48 THEN 1 ELSE 0 END) AS b_2,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  >  48
                          AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <=  72 THEN 1 ELSE 0 END) AS b_3,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  >  72
                          AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <=  96 THEN 1 ELSE 0 END) AS b_4,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  >  96
                          AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <= 120 THEN 1 ELSE 0 END) AS b_5,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  > 120
                          AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <= 144 THEN 1 ELSE 0 END) AS b_6,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  > 144
                          AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <= 168 THEN 1 ELSE 0 END) AS b_7,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  > 168
                          AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <= 192 THEN 1 ELSE 0 END) AS b_8,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  > 192
                          AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <= 216 THEN 1 ELSE 0 END) AS b_9,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  > 216
                          AND TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col}) <= 240 THEN 1 ELSE 0 END) AS b_10,
                SUM(CASE WHEN TIMESTAMPDIFF(HOUR, o.date_created_gmt, {$end_col})  > 240 THEN 1 ELSE 0 END) AS b_over_10
            FROM {$vt} v
            INNER JOIN {$ot} o ON o.id = v.order_id
            INNER JOIN {$at} a ON a.order_id = v.order_id AND a.address_type = 'shipping'
            WHERE {$end_where}
              AND v.created_at IS NOT NULL
              AND a.postcode   IS NOT NULL AND a.postcode <> ''
              {$where_carrier}
        ";
        $dist = empty($params) ? $wpdb->get_row($dist_sql, ARRAY_A) : $wpdb->get_row($wpdb->prepare($dist_sql, $params), ARRAY_A);

        if (empty($rows)) {
            echo '<p><em>' . esc_html__('No delivered vouchers yet (or run the Backfill delivery dates first).', 'ai-eshop-optimizer') . '</em></p>';
            echo '</div>';
            return;
        }

        $fmt_hours = static function ($hours) {
            if ($hours === null) { return '—'; }
            $h = (float) $hours;
            $d = $h / 24;
            return sprintf('%.1f h (%.1f d)', $h, $d);
        };
        $fmt_money = static function ($amount, $with_zero = true) {
            if ($amount === null) { return '—'; }
            $f = (float) $amount;
            if (!$with_zero && $f == 0) { return '—'; }
            return '€' . number_format($f, 2, '.', ',');
        };
        
        
        
        $fmt_timing_cell = static function ($avg, $min, $max) use ($fmt_hours) {
            $line = '<div>' . esc_html($fmt_hours($avg)) . '</div>';
            if ($min !== null && $max !== null) {
                $range = sprintf(
                    '%s – %s',
                    is_numeric($min) ? sprintf('%dh', (int) round((float) $min)) : '—',
                    is_numeric($max) ? sprintf('%dh', (int) round((float) $max)) : '—'
                );
                $line .= '<div style="font-size:11px;color:#94a3b8;">' . esc_html($range) . '</div>';
            }
            return $line;
        };

        
        
        
        $sort_num = static function ($v) {
            return $v === null ? '' : (string) ((float) $v);
        };

        
        
        
        
        
        
        
        $excluded_sql = "
            SELECT COUNT(*) AS n
            FROM {$vt} v
            INNER JOIN {$ot} o ON o.id = v.order_id
            INNER JOIN {$at} a ON a.order_id = v.order_id AND a.address_type = 'shipping'
            WHERE v.delivered_at IS NOT NULL
              AND v.created_at   IS NOT NULL
              AND a.postcode     IS NOT NULL
              AND a.postcode     <> ''
              AND NOT (v.status = 'delivered'
                       AND v.delivered_at > v.created_at
                       AND v.delivered_at > o.date_created_gmt
                       AND v.delivered_at < (NOW() + INTERVAL 1 DAY))
              {$where_carrier}
        ";
        $excluded_count = (int) (empty($params)
            ? $wpdb->get_var($excluded_sql)
            : $wpdb->get_var($wpdb->prepare($excluded_sql, $params)));
        if ($excluded_count > 0) {
            echo '<p class="description" style="font-size:12px;color:#9aa1a8;margin:6px 0 8px;">';
            echo esc_html(sprintf(
                
                _n(
                    '%d voucher with a recorded delivery date was excluded from these averages because its timestamps are inconsistent (delivery date earlier than the order or voucher creation date, status no longer "delivered", or future-dated). Most often these are still-in-transit shipments mis-flagged by the lightweight tracking call.',
                    '%d vouchers with recorded delivery dates were excluded from these averages because their timestamps are inconsistent (delivery date earlier than the order or voucher creation date, status no longer "delivered", or future-dated). Most often these are still-in-transit shipments mis-flagged by the lightweight tracking call.',
                    $excluded_count,
                    'ai-eshop-optimizer'
                ),
                $excluded_count
            ));
            echo '</p>';
        }

        
        
        
        $nonce_for_drill = wp_create_nonce('aieo_fulfillment_admin');
        echo '<table id="aieo-ff-metrics-table" class="widefat striped aieo-ff-sortable" '
            . 'data-prefix-len="' . esc_attr((string) $prefix_len) . '" '
            . 'data-carrier="' . esc_attr($filter_carrier) . '" '
            . 'data-nonce="' . esc_attr($nonce_for_drill) . '" '
            . 'style="max-width:1280px;"><thead><tr>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="text">'    . esc_html__('Postal prefix', 'ai-eshop-optimizer')         . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num" style="width:90px;">' . esc_html__('Shipments', 'ai-eshop-optimizer') . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num">' . esc_html__('Order → voucher', 'ai-eshop-optimizer')      . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num">' . esc_html__('Voucher → delivery', 'ai-eshop-optimizer')  . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num">' . esc_html__('Order → delivery', 'ai-eshop-optimizer')   . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num">' . esc_html__('Avg paid', 'ai-eshop-optimizer')           . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num">' . esc_html__('Avg cost', 'ai-eshop-optimizer')           . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num">' . esc_html__('Avg net', 'ai-eshop-optimizer')            . ' <span class="aieo-ff-sort-arrow"></span></th>';
        
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num" title="' . esc_attr(sprintf(__('Average gross profit per order = avg net product value × %d%% margin (set in Tools).', 'ai-eshop-optimizer'), (int) round($margin_pct))) . '">' . esc_html__('Avg profit', 'ai-eshop-optimizer') . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num" title="' . esc_attr__('Minimum-order break-even (order subtotal): avg courier cost ÷ margin. Below this an order does not even cover its own courier cost.', 'ai-eshop-optimizer') . '">' . esc_html__('Min order ≥', 'ai-eshop-optimizer') . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '<th class="aieo-ff-sortable-th" data-sort-type="num" title="' . esc_attr(sprintf( __('Recommended free-shipping threshold = current bar (%s) + courier cost ÷ margin, so the EXTRA turnover above the bar covers the courier cost you absorb on a free-shipping order.', 'ai-eshop-optimizer'), '€' . number_format($fs_threshold, 0))) . '">' . esc_html__('Free-ship ≥', 'ai-eshop-optimizer') . ' <span class="aieo-ff-sort-arrow"></span></th>';
        echo '</tr></thead><tbody>';

        foreach ((array) $rows as $r) { 
            $prefix = (string) ($r['prefix'] ?? ''); 
            if ($prefix === '') { continue; } 
            echo '<tr class="aieo-ff-metrics-row" data-prefix="' . esc_attr($prefix) . '" style="cursor:pointer;">';
            echo '<td data-sort="' . esc_attr($prefix)                          . '"><span class="aieo-ff-drill-arrow" style="display:inline-block;width:10px;color:#94a3b8;">▶</span> <code>' . esc_html($prefix) . '</code></td>';
            echo '<td data-sort="' . esc_attr((string) (int) $r['shipments'])    . '">' . (int) $r['shipments'] . '</td>';
            echo '<td data-sort="' . esc_attr($sort_num($r['avg_hours_order_to_voucher']))   . '">' . $fmt_timing_cell($r['avg_hours_order_to_voucher'],   $r['min_hours_order_to_voucher'],   $r['max_hours_order_to_voucher'])   . '</td>'; 
            echo '<td data-sort="' . esc_attr($sort_num($r['avg_hours_voucher_to_delivery'])). '">' . $fmt_timing_cell($r['avg_hours_voucher_to_delivery'], $r['min_hours_voucher_to_delivery'], $r['max_hours_voucher_to_delivery']) . '</td>'; 
            echo '<td data-sort="' . esc_attr($sort_num($r['avg_hours_order_to_delivery']))  . '">' . $fmt_timing_cell($r['avg_hours_order_to_delivery'],  $r['min_hours_order_to_delivery'],  $r['max_hours_order_to_delivery'])  . '</td>'; 
            echo '<td data-sort="' . esc_attr($sort_num($r['avg_customer_paid']))            . '">' . esc_html($fmt_money($r['avg_customer_paid']))            . '</td>';
            echo '<td data-sort="' . esc_attr($sort_num($r['avg_carrier_cost']))             . '">' . esc_html($fmt_money($r['avg_carrier_cost']))             . '</td>';
            echo '<td data-sort="' . esc_attr($sort_num($r['avg_net']))                      . '">' . esc_html($fmt_money($r['avg_net']))                      . '</td>';
            $avg_profit = isset($r['avg_net_products']) && $r['avg_net_products'] !== null ? (float) $r['avg_net_products'] * $margin : null;
            $min_order  = isset($r['avg_carrier_cost']) && $r['avg_carrier_cost'] !== null && $margin > 0 ? (float) $r['avg_carrier_cost'] / $margin : null;
            $free_ship  = ($min_order !== null) ? $fs_threshold + $min_order : null;
            echo '<td data-sort="' . esc_attr($sort_num($avg_profit)) . '">' . esc_html($fmt_money($avg_profit)) . '</td>';
            echo '<td data-sort="' . esc_attr($sort_num($min_order))  . '">' . esc_html($fmt_money($min_order))  . '</td>';
            echo '<td data-sort="' . esc_attr($sort_num($free_ship))  . '">' . esc_html($fmt_money($free_ship))  . '</td>';
            echo '</tr>';
        }
        echo '</tbody>';
 
        
        if (is_array($totals)) { 
            echo '<tfoot><tr style="background:#fef3c7;font-weight:600;">';
            echo '<td>' . esc_html__('All postal areas', 'ai-eshop-optimizer') . '</td>';
            echo '<td>' . (int) $totals['shipments'] . '</td>';
            echo '<td>' . $fmt_timing_cell($totals['avg_hours_order_to_voucher'],   $totals['min_hours_order_to_voucher'],   $totals['max_hours_order_to_voucher'])   . '</td>'; 
            echo '<td>' . $fmt_timing_cell($totals['avg_hours_voucher_to_delivery'], $totals['min_hours_voucher_to_delivery'], $totals['max_hours_voucher_to_delivery']) . '</td>'; 
            echo '<td>' . $fmt_timing_cell($totals['avg_hours_order_to_delivery'],  $totals['min_hours_order_to_delivery'],  $totals['max_hours_order_to_delivery'])  . '</td>'; 
            echo '<td>' . esc_html($fmt_money($totals['avg_customer_paid'])) . '</td>';
            echo '<td>' . esc_html($fmt_money($totals['avg_carrier_cost'])) . '</td>';
            echo '<td>' . esc_html($fmt_money($totals['avg_net'])) . '</td>';
            $t_profit    = isset($totals['avg_net_products']) && $totals['avg_net_products'] !== null ? (float) $totals['avg_net_products'] * $margin : null;
            $t_min_order = isset($totals['avg_carrier_cost']) && $totals['avg_carrier_cost'] !== null && $margin > 0 ? (float) $totals['avg_carrier_cost'] / $margin : null;
            $t_free_ship = ($t_min_order !== null) ? $fs_threshold + $t_min_order : null;
            echo '<td>' . esc_html($fmt_money($t_profit))    . '</td>';
            echo '<td>' . esc_html($fmt_money($t_min_order)) . '</td>';
            echo '<td>' . esc_html($fmt_money($t_free_ship)) . '</td>';
            echo '</tr></tfoot>';
        }
        echo '</table>';

        
        
        
        
        if (is_array($totals) && isset($totals['avg_carrier_cost']) && $totals['avg_carrier_cost'] !== null && $margin > 0) {
            $overall_min = (float) $totals['avg_carrier_cost'] / $margin;
            $overall_fs  = $fs_threshold + $overall_min;
            echo '<div class="aieo-dmm-card" style="margin-top:12px;background:#ecfdf5;border:1px solid #a7f3d0;max-width:1280px;">';
            echo '<h4 style="margin:0 0 6px;">' . esc_html__('Free-shipping break-even', 'ai-eshop-optimizer') . '</h4>';
            echo '<p style="margin:0;">' . sprintf(
                
                esc_html__('Average courier cost is %1$s. At a %2$d%% gross margin an order must carry %3$s of products just to cover that — the "Min order" break-even. Because you absorb the courier cost on a free-shipping order, raise your current %4$s free-shipping bar to ≥ %5$s: the extra %3$s of turnover above the bar earns ~%1$s of margin, which offsets the courier cost. The per-area "Free-ship ≥" column gives this per postal prefix (pricier regions need a higher bar).', 'ai-eshop-optimizer'),
                esc_html($fmt_money($totals['avg_carrier_cost'])),
                (int) round($margin_pct),
                '<strong>' . esc_html($fmt_money($overall_min)) . '</strong>',
                esc_html($fmt_money($fs_threshold)),
                '<strong>' . esc_html($fmt_money($overall_fs)) . '</strong>'
            ) . '</p>';
            echo '<p class="description" style="margin:6px 0 0;">' . esc_html(sprintf(
                
                __('“Avg profit” = avg net product value × %d%% margin. Set the current free-shipping bar in Tools. Rough planning estimates from a single margin figure, not per-product costs.', 'ai-eshop-optimizer'),
                (int) round($margin_pct)
            )) . '</p>';
            echo '</div>';
        }

        
        
        
        
        
        
        ?>
        <style>
            .aieo-ff-sortable-th { cursor: pointer; user-select: none; }
            .aieo-ff-sortable-th:hover { background: #f1f5f9; }
            .aieo-ff-sort-arrow { opacity: 0.45; font-size: 11px; margin-left: 3px; }
            .aieo-ff-sortable-th.aieo-sort-asc  .aieo-ff-sort-arrow::before { content: '\25B2'; opacity: 1; }
            .aieo-ff-sortable-th.aieo-sort-desc .aieo-ff-sort-arrow::before { content: '\25BC'; opacity: 1; }
        </style>
        <script>
        (function(){
            var table = document.getElementById('aieo-ff-metrics-table');
            if (!table) return;
            var tbody = table.querySelector('tbody');

            table.querySelectorAll('th.aieo-ff-sortable-th').forEach(function(th, idx){
                th.addEventListener('click', function(){
                    var type = th.getAttribute('data-sort-type') || 'text';
                    var asc  = !th.classList.contains('aieo-sort-asc'); // toggle

                    // Reset arrows on all headers, then mark this one.
                    table.querySelectorAll('th.aieo-ff-sortable-th')
                        .forEach(function(h){ h.classList.remove('aieo-sort-asc','aieo-sort-desc'); });
                    th.classList.add(asc ? 'aieo-sort-asc' : 'aieo-sort-desc');

                    var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
                    rows.sort(function(a, b){
                        var ca = a.cells[idx], cb = b.cells[idx];
                        var av = ca.getAttribute('data-sort'); if (av === null) { av = ca.textContent.trim(); }
                        var bv = cb.getAttribute('data-sort'); if (bv === null) { bv = cb.textContent.trim(); }
                        // Empty cells always sort to the bottom regardless of direction.
                        if (av === '' && bv !== '') return 1;
                        if (bv === '' && av !== '') return -1;
                        if (av === '' && bv === '') return 0;
                        if (type === 'num') {
                            var na = parseFloat(av), nb = parseFloat(bv);
                            return asc ? na - nb : nb - na;
                        }
                        return asc ? av.localeCompare(bv) : bv.localeCompare(av);
                    });
                    rows.forEach(function(r){ tbody.appendChild(r); });
                });
            });

            // ---- Drill-down: click a prefix row → modal listing the
            //      individual shipments (order id + name + address +
            //      order value, plus carrier + voucher for jump-out).
            var ajaxUrl    = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
            var prefixLen  = parseInt(table.getAttribute('data-prefix-len'), 10) || 2;
            var carrier    = table.getAttribute('data-carrier') || '';
            var nonce      = table.getAttribute('data-nonce')   || '';

            function escapeHtml(s){
                return String(s == null ? '' : s).replace(/[&<>"']/g, function(c){
                    return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
                });
            }

            // Reusable modal scaffold — built once on first open, hidden
            // by default. Closes on backdrop click, X click, or Escape.
            var modal = null, modalBody = null, modalTitle = null;
            function ensureModal(){
                if (modal) return;
                modal = document.createElement('div');
                modal.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(15,23,42,0.55);z-index:99999;align-items:flex-start;justify-content:center;padding:40px 20px;overflow:auto;';
                modal.innerHTML =
                    '<div style="background:#fff;border-radius:8px;width:100%;max-width:1100px;box-shadow:0 12px 40px rgba(0,0,0,0.25);">' +
                    '  <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-bottom:1px solid #e5e7eb;">' +
                    '    <h2 style="margin:0;font-size:16px;" class="aieo-ff-drill-title"></h2>' +
                    '    <div style="display:flex;align-items:center;gap:10px;">' +
                    '      <button type="button" class="button button-secondary aieo-ff-drill-export"><?php echo esc_js(__('Export XLSX', 'ai-eshop-optimizer')); ?></button>' +
                    '      <button type="button" class="aieo-ff-drill-close button-link" style="font-size:22px;line-height:1;color:#475569;cursor:pointer;background:none;border:none;padding:0;" aria-label="Close">&times;</button>' +
                    '    </div>' +
                    '  </div>' +
                    '  <div class="aieo-ff-drill-body" style="padding:14px 18px;max-height:70vh;overflow:auto;"></div>' +
                    '</div>';
                document.body.appendChild(modal);
                modalBody  = modal.querySelector('.aieo-ff-drill-body');
                modalTitle = modal.querySelector('.aieo-ff-drill-title');

                function closeModal(){ modal.style.display = 'none'; }
                modal.addEventListener('click', function(e){ if (e.target === modal) closeModal(); });
                modal.querySelector('.aieo-ff-drill-close').addEventListener('click', closeModal);
                document.addEventListener('keydown', function(e){
                    if (e.key === 'Escape' && modal.style.display !== 'none') closeModal();
                });

                // Export XLSX — submit a hidden form to the export
                // endpoint. The server streams the .xlsx file with the
                // right Content-Type and the browser handles download
                // natively. No fetch/blob shenanigans needed.
                modal.querySelector('.aieo-ff-drill-export').addEventListener('click', function(){
                    var prefix = modal.dataset.prefix || '';
                    var bucket = modal.dataset.bucket || '';
                    if (!prefix && !bucket) return;
                    var form = document.createElement('form');
                    form.method = 'POST';
                    form.action = ajaxUrl;
                    form.target = '_self';
                    function field(name, val){
                        var i = document.createElement('input');
                        i.type = 'hidden'; i.name = name; i.value = val;
                        form.appendChild(i);
                    }
                    field('action', 'aieo_ff_metrics_export');
                    field('nonce', nonce);
                    if (prefix) field('prefix', prefix);
                    if (bucket) field('bucket', bucket);
                    field('prefix_len', prefixLen);
                    if (carrier) field('carrier', carrier);
                    document.body.appendChild(form);
                    form.submit();
                    document.body.removeChild(form);
                });
            }
            function openModal(title, html, prefix){
                ensureModal();
                modalTitle.textContent = title;
                modalBody.innerHTML = html;
                modal.dataset.prefix = prefix || '';
                // Reset bucket — bucket-click handler sets it AFTER
                // calling openModal. This way prefix-click and bucket-
                // click never carry stale state across modal opens.
                modal.dataset.bucket = '';
                // Disable Export until the AJAX populates rows.
                var expBtn = modal.querySelector('.aieo-ff-drill-export');
                if (expBtn) expBtn.disabled = true;
                modal.style.display = 'flex';
            }
            function enableExport(){
                var expBtn = modal && modal.querySelector('.aieo-ff-drill-export');
                if (expBtn) expBtn.disabled = false;
            }

            function renderDrillBody(payload){
                if (!payload || !payload.rows || !payload.rows.length) {
                    return '<em><?php echo esc_js(__('No shipments found for this row.', 'ai-eshop-optimizer')); ?></em>';
                }
                var html = '<table class="widefat striped" style="font-size:12px;"><thead><tr>'
                    + '<th><?php echo esc_js(__('Order', 'ai-eshop-optimizer')); ?></th>'
                    + '<th><?php echo esc_js(__('Carrier', 'ai-eshop-optimizer')); ?></th>'
                    + '<th><?php echo esc_js(__('Voucher', 'ai-eshop-optimizer')); ?></th>'
                    + '<th><?php echo esc_js(__('Customer', 'ai-eshop-optimizer')); ?></th>'
                    + '<th><?php echo esc_js(__('Shipping address', 'ai-eshop-optimizer')); ?></th>'
                    + '<th style="white-space:nowrap;"><?php echo esc_js(__('Order → delivery', 'ai-eshop-optimizer')); ?></th>'
                    + '<th style="text-align:right;"><?php echo esc_js(__('Order value', 'ai-eshop-optimizer')); ?></th>'
                    + '</tr></thead><tbody>';
                payload.rows.forEach(function(r){
                    html += '<tr>'
                        + '<td><a href="' + escapeHtml(r.order_admin_url) + '" target="_blank" rel="noopener">#' + (r.order_id|0) + '</a></td>'
                        + '<td>' + escapeHtml(r.carrier_label) + '</td>'
                        + '<td>' + (r.tracking_url
                                ? '<a href="' + escapeHtml(r.tracking_url) + '" target="_blank" rel="noopener"><code>' + escapeHtml(r.voucher_no) + '</code></a>'
                                : '<code>' + escapeHtml(r.voucher_no) + '</code>') + '</td>'
                        + '<td>' + escapeHtml(r.customer_name) + '</td>'
                        + '<td style="font-size:11px;color:#475569;">' + escapeHtml(r.address) + '</td>'
                        + '<td style="white-space:nowrap;">' + escapeHtml(r.order_to_delivery) + '</td>'
                        + '<td style="text-align:right;white-space:nowrap;">' + escapeHtml(r.order_total) + '</td>'
                        + '</tr>';
                });
                html += '</tbody></table>';
                if (payload.capped) {
                    html += '<p class="description" style="margin-top:6px;font-size:11px;color:#b45309;"><?php echo esc_js(__('Showing the 250 most-recent shipments. Older ones are still counted in the row averages.', 'ai-eshop-optimizer')); ?></p>';
                }
                return html;
            }

            // ---- Distribution bucket click → modal for that bucket
            //      (no prefix filter; only carrier + bucket). Bind
            //      directly to each row so propagation / parent
            //      pointer-events / browser quirks can't swallow the
            //      click — the earlier delegate approach intermittently
            //      didn't fire.
            var bucketLabels = {
                '2':       '<?php echo esc_js(__('Within 2 days', 'ai-eshop-optimizer')); ?>',
                '3':       '<?php echo esc_js(__('3 days', 'ai-eshop-optimizer')); ?>',
                '4':       '<?php echo esc_js(__('4 days', 'ai-eshop-optimizer')); ?>',
                '5':       '<?php echo esc_js(__('5 days', 'ai-eshop-optimizer')); ?>',
                '6':       '<?php echo esc_js(__('6 days', 'ai-eshop-optimizer')); ?>',
                '7':       '<?php echo esc_js(__('7 days', 'ai-eshop-optimizer')); ?>',
                '8':       '<?php echo esc_js(__('8 days', 'ai-eshop-optimizer')); ?>',
                '9':       '<?php echo esc_js(__('9 days', 'ai-eshop-optimizer')); ?>',
                '10':      '<?php echo esc_js(__('10 days', 'ai-eshop-optimizer')); ?>',
                'over_10': '<?php echo esc_js(__('Over 10 days', 'ai-eshop-optimizer')); ?>'
            };

            function openBucketModal(bucket){
                if (!bucket || !bucketLabels[bucket]) return;
                var carrierLabel = carrier ? carrier : '<?php echo esc_js(__('all carriers', 'ai-eshop-optimizer')); ?>';
                openModal(
                    '<?php echo esc_js(__('Shipments — delivery time', 'ai-eshop-optimizer')); ?> ' + bucketLabels[bucket]
                        + ' · ' + carrierLabel,
                    '<em><?php echo esc_js(__('Loading…', 'ai-eshop-optimizer')); ?></em>',
                    ''
                );
                if (modal) modal.dataset.bucket = bucket;
                var fd = new FormData();
                fd.set('action', 'aieo_ff_metrics_drill');
                fd.set('nonce', nonce);
                fd.set('bucket', bucket);
                fd.set('prefix_len', prefixLen);
                if (carrier) fd.set('carrier', carrier);
                fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd})
                  .then(function(r){return r.json();})
                  .then(function(j){
                    if (!j || !j.success) {
                        modalBody.innerHTML = '<em style="color:#b91c1c;">'
                            + escapeHtml((j && j.data && j.data.message) || 'Failed to load shipments.')
                            + '</em>';
                        return;
                    }
                    modalBody.innerHTML = renderDrillBody(j.data || {});
                    if (j.data && j.data.rows && j.data.rows.length) { enableExport(); }
                  })
                  .catch(function(err){
                    modalBody.innerHTML = '<em style="color:#b91c1c;">' + escapeHtml(String(err)) + '</em>';
                  });
            }

            document.querySelectorAll('.aieo-ff-dist-row[data-bucket]').forEach(function(row){
                // Skip rows that PHP marked disabled (count = 0).
                if (row.style.opacity && parseFloat(row.style.opacity) < 1) return;
                row.addEventListener('click', function(){
                    openBucketModal(row.getAttribute('data-bucket'));
                });
            });

            tbody.addEventListener('click', function(e){
                if (e.target.closest('a, button')) return;
                var tr = e.target.closest('tr.aieo-ff-metrics-row');
                if (!tr) return;
                var prefix = tr.getAttribute('data-prefix') || '';

                var carrierLabel = carrier ? carrier : '<?php echo esc_js(__('all carriers', 'ai-eshop-optimizer')); ?>';
                openModal(
                    '<?php echo esc_js(__('Shipments — postal area', 'ai-eshop-optimizer')); ?> ' + prefix
                        + ' · ' + carrierLabel,
                    '<em><?php echo esc_js(__('Loading…', 'ai-eshop-optimizer')); ?></em>',
                    prefix
                );

                var fd = new FormData();
                fd.set('action', 'aieo_ff_metrics_drill');
                fd.set('nonce', nonce);
                fd.set('prefix', prefix);
                fd.set('prefix_len', prefixLen);
                if (carrier) fd.set('carrier', carrier);
                fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd})
                  .then(function(r){return r.json();})
                  .then(function(j){
                    if (!j || !j.success) {
                        modalBody.innerHTML = '<em style="color:#b91c1c;">'
                            + escapeHtml((j && j.data && j.data.message) || 'Failed to load shipments.')
                            + '</em>';
                        return;
                    }
                    modalBody.innerHTML = renderDrillBody(j.data || {});
                    if (j.data && j.data.rows && j.data.rows.length) { enableExport(); }
                  })
                  .catch(function(err){
                    modalBody.innerHTML = '<em style="color:#b91c1c;">' + escapeHtml(String(err)) + '</em>';
                  });
            });
        })();
        </script>
        <?php
        echo '<p class="description" style="margin-top:8px;"><em>' . esc_html__('Avg paid = customer-paid shipping (excl. tax / COD); Avg cost = quoted carrier cost; Avg net = paid − cost (only counts shipments where carrier cost was captured at issuance).', 'ai-eshop-optimizer') . '</em></p>';

        
        if (is_array($dist)) {
            $bucket_defs = array(
                '2'       => array('label' => __('Within 2 days', 'ai-eshop-optimizer'), 'color' => '#15803d'),
                '3'       => array('label' => __('3 days', 'ai-eshop-optimizer'),        'color' => '#22c55e'),
                '4'       => array('label' => __('4 days', 'ai-eshop-optimizer'),        'color' => '#65a30d'),
                '5'       => array('label' => __('5 days', 'ai-eshop-optimizer'),        'color' => '#84cc16'),
                '6'       => array('label' => __('6 days', 'ai-eshop-optimizer'),        'color' => '#eab308'),
                '7'       => array('label' => __('7 days', 'ai-eshop-optimizer'),        'color' => '#f59e0b'),
                '8'       => array('label' => __('8 days', 'ai-eshop-optimizer'),        'color' => '#f97316'),
                '9'       => array('label' => __('9 days', 'ai-eshop-optimizer'),        'color' => '#ea580c'),
                '10'      => array('label' => __('10 days', 'ai-eshop-optimizer'),       'color' => '#c2410c'),
                'over_10' => array('label' => __('Over 10 days', 'ai-eshop-optimizer'),  'color' => '#dc2626'),
            );
            $max_count = 0;
            foreach ($bucket_defs as $key => $_) {
                $c = (int) ($dist['b_' . $key] ?? 0);
                if ($c > $max_count) { $max_count = $c; }
            }
            $total_count = (int) (is_array($totals) ? $totals['shipments'] : 0);

            echo '<h4 style="margin-top:18px;">' . esc_html__('Delivery-time distribution', 'ai-eshop-optimizer') . '</h4>';

            
            
            
            
            
            
            
            $base_url     = admin_url('admin.php?page=' . AIEO_DMM::MENU_SLUG . '&dmm_tab=fulfillment&ff_section=runlog');
            $url_total    = add_query_arg(array('metrics_basis' => 'total',       'ff_metrics_carrier' => $filter_carrier), $base_url); 
            $url_fulfill  = add_query_arg(array('metrics_basis' => 'fulfillment', 'ff_metrics_carrier' => $filter_carrier), $base_url);
            echo '<p style="margin:6px 0;font-size:12px;">';
            echo '<strong>' . esc_html__('Time basis:', 'ai-eshop-optimizer') . '</strong> '; 
            $active_attr = 'style="font-weight:700;text-decoration:none;border-bottom:2px solid #2563eb;padding-bottom:1px;"';
            $inactive_attr = 'style="color:#475569;"';
            echo '<a href="' . esc_url($url_total) . '" ' . ($metrics_basis === 'total' ? $active_attr : $inactive_attr) . '>' 
                . esc_html__('Order → delivery (total)', 'ai-eshop-optimizer') . '</a>';
            echo ' &nbsp;·&nbsp; ';
            echo '<a href="' . esc_url($url_fulfill) . '" ' . ($metrics_basis === 'fulfillment' ? $active_attr : $inactive_attr) . '>' 
                . esc_html__('Order → arrived at delivery zone (fulfillment lag)', 'ai-eshop-optimizer') . '</a>';
            echo '</p>';

            $desc = $metrics_basis === 'fulfillment'
                ? __('Showing how long our half of the journey took (order placed → parcel out for delivery), excluding recipient response time. Only counts shipments with a captured "out for delivery" (C_A3) checkpoint — historical rows tracked only via the lightweight call do not have this and are excluded. New shipments populate it automatically via the nightly poll.', 'ai-eshop-optimizer')
                : __('Showing the total customer-perceived wait time (order placed → recipient took possession). Mixes our fulfillment lag with any recipient-response delay (absences, voluntary pickup-after-attempt). Switch to "fulfillment lag" above for the operational view.', 'ai-eshop-optimizer');
            echo '<p class="description">' . esc_html($desc) . ' ' . esc_html__('Click any bar to see the orders in that bucket. Buckets are exclusive: "3 days" = strictly more than 2 days and at most 3 days, etc. Respects the active carrier filter.', 'ai-eshop-optimizer') . '</p>';
            echo '<div class="aieo-ff-dist" style="max-width:640px;font-size:13px;">';
            foreach ($bucket_defs as $key => $def) {
                $count   = (int) ($dist['b_' . $key] ?? 0);
                $pct     = $total_count > 0 ? round(100 * $count / $total_count, 1) : 0;
                $bar_pct = $max_count > 0 ? round(100 * $count / $max_count, 1) : 0;
                $disabled = $count === 0;
                echo '<div class="aieo-ff-dist-row" data-bucket="' . esc_attr($key) . '"'
                    . ' style="display:flex;align-items:center;gap:10px;padding:6px 0;cursor:' . ($disabled ? 'default' : 'pointer') . ';' . ($disabled ? 'opacity:0.5;' : '') . '">';
                echo '<div style="width:120px;flex-shrink:0;">' . esc_html($def['label']) . '</div>';
                echo '<div style="flex:1;background:#f1f5f9;border-radius:4px;height:18px;position:relative;overflow:hidden;">';
                echo '<div style="background:' . esc_attr($def['color']) . ';height:100%;width:' . esc_attr($bar_pct) . '%;transition:opacity 0.15s;"></div>';
                echo '</div>';
                echo '<div style="width:140px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums;">'
                    . '<strong>' . number_format($count) . '</strong>'
                    . ' <span style="color:#64748b;font-size:11px;">(' . number_format($pct, 1) . '%)</span>'
                    . '</div>';
                echo '</div>';
            }
            echo '</div>';
        }

        
        $costed_rows = array();
        foreach ((array) $rows as $r) {
            if (!empty($r['shipments_with_cost']) && (int) $r['shipments_with_cost'] >= 5) {
                $costed_rows[] = $r;
            }
        }
        if (!empty($costed_rows)) {
            usort($costed_rows, static function ($a, $b) {
                return ((float) $b['avg_net']) <=> ((float) $a['avg_net']);
            });
            $best  = array_slice($costed_rows, 0, 10);
            $worst = array_slice($costed_rows, -10);
            $worst = array_reverse($worst); 
            echo '<h4 style="margin-top:18px;">' . esc_html__('Best vs worst postal areas (by avg net per shipment)', 'ai-eshop-optimizer') . '</h4>';
            echo '<p class="description">' . esc_html__('Only prefixes with at least 5 cost-captured shipments are ranked, so a single low-cost outlier can\'t skew the lists. Negative net = we paid more for shipping than the customer paid us.', 'ai-eshop-optimizer') . '</p>';
            echo '<div style="display:flex;gap:18px;flex-wrap:wrap;">';

            $render_list = static function ($title, $list, $fmt_money) {
                echo '<div style="flex:1;min-width:340px;">';
                echo '<h5 style="margin:6px 0;">' . esc_html($title) . '</h5>';
                echo '<table class="widefat striped"><thead><tr>';
                echo '<th>' . esc_html__('Prefix', 'ai-eshop-optimizer') . '</th>';
                echo '<th>' . esc_html__('Shipments (with cost)', 'ai-eshop-optimizer') . '</th>';
                echo '<th>' . esc_html__('Avg net', 'ai-eshop-optimizer') . '</th>';
                echo '<th>' . esc_html__('Total net', 'ai-eshop-optimizer') . '</th>';
                echo '</tr></thead><tbody>';
                foreach ($list as $r) {
                    echo '<tr>';
                    echo '<td><code>' . esc_html((string) $r['prefix']) . '</code></td>';
                    echo '<td>' . (int) $r['shipments'] . ' (' . (int) $r['shipments_with_cost'] . ')</td>';
                    echo '<td>' . esc_html($fmt_money($r['avg_net'])) . '</td>';
                    echo '<td>' . esc_html($fmt_money($r['sum_net'])) . '</td>';
                    echo '</tr>';
                }
                echo '</tbody></table></div>';
            };
            $render_list(__('Most profitable (top 10)', 'ai-eshop-optimizer'), $best, $fmt_money);
            $render_list(__('Least profitable (bottom 10)', 'ai-eshop-optimizer'), $worst, $fmt_money);
            echo '</div>';
        } else {
            echo '<p class="description" style="margin-top:14px;"><em>' . esc_html__('Best/worst ranking will appear here once enough vouchers have a captured carrier cost (≥5 per prefix). Cost is captured automatically for new vouchers from now on.', 'ai-eshop-optimizer') . '</em></p>';
        }

        echo '</div>';
    }

    
    public function ajax_save_general_settings() {
        $this->verify_admin_ajax();

        $couriers = $this->couriers();
        $default_carrier = isset($_POST['default_carrier'])
            ? sanitize_key((string) wp_unslash($_POST['default_carrier']))
            : '';
        if ($default_carrier !== '' && !isset($couriers[$default_carrier])) {
            $default_carrier = '';
        }

        $prefix_len = isset($_POST['metrics_postcode_prefix_length'])
            ? (int) $_POST['metrics_postcode_prefix_length']
            : 2;
        if ($prefix_len < 1 || $prefix_len > 5) { $prefix_len = 2; }

        $manifest_footer = isset($_POST['manifest_footer'])
            ? sanitize_text_field((string) wp_unslash($_POST['manifest_footer']))
            : '';

        AIEO_Fulfillment_Settings::set_many(array(
            'default_carrier'                => $default_carrier,
            'metrics_postcode_prefix_length' => $prefix_len,
            'manifest_footer'                => $manifest_footer,
        ));

        wp_send_json_success(array(
            'default_carrier'                => $default_carrier,
            'metrics_postcode_prefix_length' => $prefix_len,
            'manifest_footer'                => $manifest_footer,
            'message'                        => __('Settings saved.', 'ai-eshop-optimizer'),
        ));
    }

    private function render_settings() {
        
        $this->render_general_settings_card();

        
        $this->render_activity_counters_card();

        
        $this->render_merge_couriers_section();

        $all = AIEO_Fulfillment_Settings::all();
        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Schema state', 'ai-eshop-optimizer') . '</h3>';
        global $wpdb;
        $expected = array(
            'aieo_dmm_fulfillment_vouchers',
            'aieo_dmm_fulfillment_checkpoints',
            'aieo_dmm_fulfillment_documents',
            'aieo_dmm_fulfillment_sequences',
            'aieo_dmm_fulfillment_settings',
            'aieo_dmm_fulfillment_closings',
        );
        echo '<table class="widefat fixed striped"><tbody>';
        foreach ($expected as $name) {
            $full   = $wpdb->prefix . $name;
            $exists = (bool) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $full));
            echo '<tr><td><code>' . esc_html($full) . '</code></td>';
            echo '<td>' . ($exists ? '<span style="color:#15803d;">✓ present</span>' : '<span style="color:#b91c1c;">✗ missing</span>') . '</td></tr>';
        }
        echo '</tbody></table>';

        echo '<h3 style="margin-top:18px;">' . esc_html__('Settings rows', 'ai-eshop-optimizer') . '</h3>';
        if (empty($all)) {
            echo '<p><em>' . esc_html__('No settings yet. The settings save UI lands in later phases — for now use AIEO_Fulfillment_Settings::set() from wp-cli.', 'ai-eshop-optimizer') . '</em></p>';
        } else {
            echo '<table class="widefat fixed striped"><thead><tr>';
            echo '<th>' . esc_html__('Key', 'ai-eshop-optimizer') . '</th>';
            echo '<th>' . esc_html__('Value (truncated)', 'ai-eshop-optimizer') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($all as $k => $v) {
                $serialized = is_scalar($v) ? (string) $v : wp_json_encode($v);
                $shown = mb_strlen($serialized) > 200 ? mb_substr($serialized, 0, 200) . '…' : $serialized;
                echo '<tr><td><code>' . esc_html($k) . '</code></td><td><code style="font-size:11px;">' . esc_html($shown) . '</code></td></tr>';
            }
            echo '</tbody></table>';
        }
        echo '</div>';

        
        $this->render_migration_card();

        
        $this->render_backfill_card();
    }

    
    private function render_general_settings_card() {
        $couriers   = $this->couriers();
        $current_default = (string) AIEO_Fulfillment_Settings::get('default_carrier', '');
        $prefix_len      = (int) AIEO_Fulfillment_Settings::get('metrics_postcode_prefix_length', 2);
        if ($prefix_len < 1 || $prefix_len > 5) { $prefix_len = 2; }
        $manifest_footer = (string) AIEO_Fulfillment_Settings::get('manifest_footer', '');
        $nonce = wp_create_nonce('aieo_fulfillment_admin');

        echo '<div class="aieo-dmm-card" data-aieo-ff-general data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('General', 'ai-eshop-optimizer') . '</h3>';
        echo '<table class="form-table" role="presentation"><tbody>';

        
        
        
        
        echo '<tr><th scope="row"><label for="aieo-ff-default-carrier">' . esc_html__('Default carrier', 'ai-eshop-optimizer') . '</label></th><td>';
        echo '<select id="aieo-ff-default-carrier">';
        echo '<option value="">— ' . esc_html__('first in list', 'ai-eshop-optimizer') . ' —</option>';
        foreach ($couriers as $code => $driver) {
            echo '<option value="' . esc_attr($code) . '"' . selected($current_default, $code, false) . '>' . esc_html($driver->label()) . '</option>';
        }
        echo '</select>';
        echo '<p class="description">' . esc_html__('Pre-selected when creating a voucher. Set to "Geniki" if 90% of your shipments go through Geniki — it saves a click per order.', 'ai-eshop-optimizer') . '</p>';
        echo '</td></tr>';

        
        echo '<tr><th scope="row"><label for="aieo-ff-prefix-len">' . esc_html__('Metrics postcode grouping', 'ai-eshop-optimizer') . '</label></th><td>';
        echo '<select id="aieo-ff-prefix-len">';
        $labels = array(
            1 => __('1 digit (very coarse)', 'ai-eshop-optimizer'),
            2 => __('2 digits (nomos / county — robust averages)', 'ai-eshop-optimizer'),
            3 => __('3 digits (town / zone — finer, may be sparse for low-volume areas)', 'ai-eshop-optimizer'),
            4 => __('4 digits', 'ai-eshop-optimizer'),
            5 => __('5 digits (each unique postcode)', 'ai-eshop-optimizer'),
        );
        foreach ($labels as $n => $label) {
            echo '<option value="' . (int) $n . '"' . selected($prefix_len, $n, false) . '>' . esc_html($label) . '</option>';
        }
        echo '</select>';
        echo '<p class="description">' . esc_html__('Used by the Delivery Statistics → "Delivery time metrics by postal area" table. Spaces in postcodes are stripped before extracting the prefix.', 'ai-eshop-optimizer') . '</p>';
        echo '</td></tr>';

        
        
        
        echo '<tr><th scope="row"><label for="aieo-ff-manifest-footer">' . esc_html__('Manifest footer', 'ai-eshop-optimizer') . '</label></th><td>';
        echo '<input type="text" id="aieo-ff-manifest-footer" class="regular-text" value="' . esc_attr($manifest_footer) . '" placeholder="' . esc_attr__('On behalf of … company name', 'ai-eshop-optimizer') . '" style="width:520px;max-width:100%;">';
        echo '<p class="description">' . esc_html__('Sender line printed at the bottom of every end-of-day courier manifest (e.g. "On behalf of … company name"). Leave blank to fall back to the shop name.', 'ai-eshop-optimizer') . '</p>';
        echo '</td></tr>';

        echo '</tbody></table>';
        echo '<p><button type="button" class="button button-primary" id="aieo-ff-general-save">' . esc_html__('Save', 'ai-eshop-optimizer') . '</button> ';
        echo '<span id="aieo-ff-general-status" style="margin-left:8px;font-size:13px;color:#646970;"></span></p>';

        ?>
        <script>
        (function(){
            var card = document.querySelector('[data-aieo-ff-general]'); if (!card) return;
            var nonce = card.getAttribute('data-nonce');
            var ajaxUrl = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
            var btn = document.getElementById('aieo-ff-general-save');
            var status = document.getElementById('aieo-ff-general-status');
            btn.addEventListener('click', function(){
                btn.disabled = true;
                status.style.color = '#646970';
                status.textContent = '<?php echo esc_js(__('Saving…', 'ai-eshop-optimizer')); ?>';
                var fd = new FormData();
                fd.set('action', 'aieo_ff_save_general');
                fd.set('nonce', nonce);
                fd.set('default_carrier', document.getElementById('aieo-ff-default-carrier').value);
                fd.set('metrics_postcode_prefix_length', document.getElementById('aieo-ff-prefix-len').value);
                fd.set('manifest_footer', (document.getElementById('aieo-ff-manifest-footer')||{}).value || '');
                fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd})
                  .then(function(r){return r.json();})
                  .then(function(j){
                    btn.disabled = false;
                    if (j && j.success) {
                        status.style.color = '#15803d';
                        status.textContent = (j.data && j.data.message) || '<?php echo esc_js(__('Saved.', 'ai-eshop-optimizer')); ?>';
                    } else {
                        status.style.color = '#b91c1c';
                        status.textContent = (j && j.data && j.data.message) || '<?php echo esc_js(__('Save failed.', 'ai-eshop-optimizer')); ?>';
                    }
                  })
                  .catch(function(err){ btn.disabled = false; status.style.color='#b91c1c'; status.textContent = String(err); });
            });
        })();
        </script>
        <?php
        echo '</div>';
    }

    
    private function render_backfill_card() {
        $nonce    = wp_create_nonce('aieo_fulfillment_admin');
        $couriers = $this->couriers();
        
        
        $candidates = array();
        foreach ($couriers as $code => $driver) {
            if ($driver->has_capability('track')) {
                $candidates[$code] = $driver->label();
            }
        }

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        echo '<div class="aieo-dmm-card" style="margin-top:18px;" data-aieo-ff-backfill data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('Backfill delivery dates', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html__('Polls the carrier for every voucher missing a delivery date and writes it back. Retrospective — covers historical shipments. Uses the lightweight tracking call (one round-trip per voucher, no checkpoint history).', 'ai-eshop-optimizer') . '</p>';

        if (empty($candidates)) {
            echo '<p><em>' . esc_html__('No trackable carriers configured.', 'ai-eshop-optimizer') . '</em></p>';
            echo '</div>';
            return;
        }

        
        
        echo '<table class="widefat striped" style="max-width:560px;margin-bottom:10px;"><thead><tr>';
        echo '<th>' . esc_html__('Carrier', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:120px;">' . esc_html__('Pending', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:120px;">' . esc_html__('Delivered', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:160px;">' . esc_html__('Action', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        foreach ($candidates as $code => $label) {
            $pending = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE carrier=%s AND delivered_at IS NULL AND status NOT IN ('canceled','returned')",
                $code
            ));
            $delivered = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE carrier=%s AND delivered_at IS NOT NULL",
                $code
            ));
            echo '<tr data-carrier="' . esc_attr($code) . '">';
            echo '<td><strong>' . esc_html($label) . '</strong></td>';
            echo '<td><span class="aieo-ff-bf-pending">' . (int) $pending . '</span></td>';
            echo '<td><span class="aieo-ff-bf-delivered">' . (int) $delivered . '</span></td>';
            echo '<td>';
            if ($pending > 0) {
                echo '<button type="button" class="button button-primary button-small aieo-ff-bf-run">' . esc_html__('Backfill', 'ai-eshop-optimizer') . '</button>';
            } else {
                echo '<em>' . esc_html__('all caught up', 'ai-eshop-optimizer') . '</em>';
            }
            echo '</td></tr>';
        }
        echo '</tbody></table>';

        echo '<p style="margin:6px 0 10px;font-size:12px;">';
        echo '<label style="cursor:pointer;"><input type="checkbox" id="aieo-ff-bf-sweep"> ';
        echo '<strong>' . esc_html__('Sweep mode', 'ai-eshop-optimizer') . '</strong> — ';
        echo esc_html__('disable the 5-consecutive-error abort and march through every remaining row, marking dead vouchers untrackable as it goes. Use when most of the pending pool is known to be from old credentials.', 'ai-eshop-optimizer');
        echo '</label></p>';

        echo '<div class="aieo-ff-bf-progress" style="display:none;margin-top:8px;">';
        echo '<div style="background:#e5e7eb;height:14px;border-radius:7px;overflow:hidden;max-width:560px;">';
        echo '<div class="aieo-ff-bf-bar" style="background:#2563eb;height:100%;width:0%;transition:width 0.2s;"></div>';
        echo '</div>';
        echo '<p class="aieo-ff-bf-status" style="font-size:13px;color:#646970;margin:6px 0 0;"></p>';
        echo '<pre class="aieo-ff-bf-errors" style="display:none;max-height:200px;overflow:auto;background:#fef2f2;padding:8px;font-size:11px;border-radius:4px;margin-top:8px;"></pre>';
        echo '</div>';

        ?>
        <script>
        (function(){
            var card = document.querySelector('[data-aieo-ff-backfill]');
            if (!card) return;
            var nonce = card.getAttribute('data-nonce');
            var bar    = card.querySelector('.aieo-ff-bf-bar');
            var status = card.querySelector('.aieo-ff-bf-status');
            var errors = card.querySelector('.aieo-ff-bf-errors');
            var prog   = card.querySelector('.aieo-ff-bf-progress');
            var ajaxUrl = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';

            card.addEventListener('click', function(e){
                var btn = e.target.closest('.aieo-ff-bf-run'); if (!btn) return;
                var row = btn.closest('tr[data-carrier]'); if (!row) return;
                var carrier = row.getAttribute('data-carrier');

                // Per-carrier progress takes the page over.
                document.querySelectorAll('.aieo-ff-bf-run').forEach(function(b){ b.disabled = true; });
                prog.style.display = 'block';
                errors.style.display = 'none';
                errors.textContent = '';
                bar.style.width = '0%';

                var processed = 0;
                var deliveredTotal = 0;
                var pendingPolled = 0;
                var untrackableTotal = 0;
                var allErrors = [];
                var consecutiveErrors = 0;
                var initialPending = parseInt(row.querySelector('.aieo-ff-bf-pending').textContent, 10) || 0;

                var sweepMode = !!(document.getElementById('aieo-ff-bf-sweep') && document.getElementById('aieo-ff-bf-sweep').checked);

                function tick() {
                    var fd = new FormData();
                    fd.set('action', 'aieo_ff_backfill_delivered');
                    fd.set('nonce', nonce);
                    fd.set('carrier', carrier);
                    fd.set('batch', '10');
                    fd.set('consecutive_errors', consecutiveErrors);
                    if (sweepMode) { fd.set('sweep', '1'); }
                    fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd})
                      .then(function(r){return r.json();})
                      .then(function(j){
                        if (!j || !j.success) {
                            status.textContent = (j && j.data && j.data.message) || '<?php echo esc_js(__('Backfill failed.', 'ai-eshop-optimizer')); ?>';
                            status.style.color = '#b91c1c';
                            document.querySelectorAll('.aieo-ff-bf-run').forEach(function(b){ b.disabled = false; });
                            return;
                        }
                        var d = j.data || {};
                        processed += d.processed || 0;
                        deliveredTotal += d.delivered || 0;
                        pendingPolled += d.pending || 0;
                        untrackableTotal += d.untrackable || 0;
                        consecutiveErrors = d.consecutive_errors || 0;
                        if (d.errors && d.errors.length) { allErrors = allErrors.concat(d.errors); }

                        var pct = initialPending > 0 ? Math.min(100, Math.round((processed / initialPending) * 100)) : 100;
                        bar.style.width = pct + '%';
                        status.textContent = '<?php echo esc_js(__('Processed', 'ai-eshop-optimizer')); ?>: ' + processed
                            + ' · <?php echo esc_js(__('Delivered', 'ai-eshop-optimizer')); ?>: ' + deliveredTotal
                            + ' · <?php echo esc_js(__('Still pending', 'ai-eshop-optimizer')); ?>: ' + pendingPolled
                            + (untrackableTotal ? ' · ' + untrackableTotal + ' <?php echo esc_js(__('untrackable', 'ai-eshop-optimizer')); ?>' : '')
                            + (allErrors.length ? ' · ' + allErrors.length + ' <?php echo esc_js(__('errors', 'ai-eshop-optimizer')); ?>' : '');
                        if (allErrors.length) {
                            errors.style.display = 'block';
                            errors.textContent = allErrors.slice(-200).join('\n');
                        }

                        if (d.complete) {
                            if (d.aborted_consecutive) {
                                status.style.color = '#b45309';
                                status.textContent += ' · <?php echo esc_js(__('Stopped: 5 consecutive untrackable/error vouchers. Likely walked into vouchers issued under different credentials — tick Sweep mode and re-run to march through the rest.', 'ai-eshop-optimizer')); ?>';
                                document.querySelectorAll('.aieo-ff-bf-run').forEach(function(b){ b.disabled = false; });
                            } else {
                                status.style.color = '#15803d';
                                status.textContent += ' · <?php echo esc_js(__('Done.', 'ai-eshop-optimizer')); ?>';
                                // Refresh so the carrier-summary table reflects new totals.
                                setTimeout(function(){ location.reload(); }, 800);
                            }
                        } else {
                            // Throttle slightly so we don't hammer the carrier API.
                            setTimeout(tick, 250);
                        }
                      })
                      .catch(function(err){
                        status.textContent = String(err);
                        status.style.color = '#b91c1c';
                        document.querySelectorAll('.aieo-ff-bf-run').forEach(function(b){ b.disabled = false; });
                      });
                }
                tick();
            });
        })();
        </script>
        <?php
        echo '</div>';

        
        $this->render_customer_paid_backfill_card();

        
        $this->render_cost_backfill_card();

        
        
        
        $this->render_carrier_invoice_import_card();

        
        
        
        
        
        
        $this->render_orphan_invoice_lines_card();

        
        $this->render_detailed_backfill_card();
    }

    
    private function render_detailed_backfill_card() {
        $nonce    = wp_create_nonce('aieo_fulfillment_admin');
        $couriers = $this->couriers();
        $candidates = array();
        foreach ($couriers as $code => $driver) {
            if ($driver->has_capability('track')) {
                $candidates[$code] = $driver->label();
            }
        }

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        echo '<div class="aieo-dmm-card" style="margin-top:18px;" data-aieo-ff-detailed-backfill data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('Backfill detailed stats', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html__('Re-polls already-tracked vouchers via the full TrackAndTrace method so we capture the checkpoint timeline and the first "out for delivery" (arrived_at) timestamp. Use this to populate fulfillment-lag analytics for historical shipments. Targets only rows that already have a delivery date but no arrived_at — the lightweight backfill never collected this. ~600ms per voucher; runs in batches of 10.', 'ai-eshop-optimizer') . '</p>';

        if (empty($candidates)) {
            echo '<p><em>' . esc_html__('No carriers advertise tracking.', 'ai-eshop-optimizer') . '</em></p>';
            echo '</div>';
            return;
        }

        echo '<table class="widefat striped" style="max-width:560px;margin-bottom:10px;"><thead><tr>';
        echo '<th>' . esc_html__('Carrier', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:140px;">' . esc_html__('Missing arrived_at', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:140px;">' . esc_html__('Already deep-tracked', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:160px;">' . esc_html__('Action', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        foreach ($candidates as $code => $label) {
            $missing = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$table}
                 WHERE carrier=%s AND delivered_at IS NOT NULL AND arrived_at IS NULL
                   AND status NOT IN ('canceled','returned','untrackable')",
                $code
            ));
            $populated = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE carrier=%s AND arrived_at IS NOT NULL",
                $code
            ));
            echo '<tr data-carrier="' . esc_attr($code) . '">';
            echo '<td><strong>' . esc_html($label) . '</strong></td>';
            echo '<td><span class="aieo-ff-db-missing">' . (int) $missing . '</span></td>';
            echo '<td><span class="aieo-ff-db-populated">' . (int) $populated . '</span></td>';
            echo '<td>';
            if ($missing > 0) {
                echo '<button type="button" class="button button-primary button-small aieo-ff-db-run">' . esc_html__('Backfill detailed', 'ai-eshop-optimizer') . '</button>';
            } else {
                echo '<em>' . esc_html__('all caught up', 'ai-eshop-optimizer') . '</em>';
            }
            echo '</td></tr>';
        }
        echo '</tbody></table>';

        echo '<p style="margin:6px 0 10px;font-size:12px;">';
        echo '<label style="cursor:pointer;"><input type="checkbox" id="aieo-ff-db-sweep"> ';
        echo '<strong>' . esc_html__('Sweep mode', 'ai-eshop-optimizer') . '</strong> — ';
        echo esc_html__('disable the 5-consecutive-error abort and march through every remaining row.', 'ai-eshop-optimizer');
        echo '</label></p>';

        echo '<div class="aieo-ff-db-progress" style="display:none;margin-top:8px;">';
        echo '<div style="background:#e5e7eb;height:14px;border-radius:7px;overflow:hidden;max-width:560px;">';
        echo '<div class="aieo-ff-db-bar" style="background:#2563eb;height:100%;width:0%;transition:width 0.2s;"></div>';
        echo '</div>';
        echo '<p class="aieo-ff-db-status" style="font-size:13px;color:#646970;margin:6px 0 0;"></p>';
        echo '<pre class="aieo-ff-db-errors" style="display:none;max-height:200px;overflow:auto;background:#fef2f2;padding:8px;font-size:11px;border-radius:4px;margin-top:8px;"></pre>';
        echo '</div>';

        ?>
        <script>
        (function(){
            var card = document.querySelector('[data-aieo-ff-detailed-backfill]');
            if (!card) return;
            var nonce  = card.getAttribute('data-nonce');
            var bar    = card.querySelector('.aieo-ff-db-bar');
            var status = card.querySelector('.aieo-ff-db-status');
            var errors = card.querySelector('.aieo-ff-db-errors');
            var prog   = card.querySelector('.aieo-ff-db-progress');
            var ajaxUrl = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';

            card.addEventListener('click', function(e){
                var btn = e.target.closest('.aieo-ff-db-run'); if (!btn) return;
                var row = btn.closest('tr[data-carrier]'); if (!row) return;
                var carrier = row.getAttribute('data-carrier');

                document.querySelectorAll('.aieo-ff-db-run').forEach(function(b){ b.disabled = true; });
                prog.style.display = 'block';
                errors.style.display = 'none';
                errors.textContent = '';
                bar.style.width = '0%';

                var processed = 0;
                var arrivedTotal = 0;
                var noC3Total    = 0;
                var untrackableTotal = 0;
                var consecutiveErrors = 0;
                var allErrors = [];
                var initialMissing = parseInt(row.querySelector('.aieo-ff-db-missing').textContent, 10) || 0;
                var sweepMode = !!(document.getElementById('aieo-ff-db-sweep') && document.getElementById('aieo-ff-db-sweep').checked);

                function tick() {
                    var fd = new FormData();
                    fd.set('action', 'aieo_ff_backfill_detailed');
                    fd.set('nonce', nonce);
                    fd.set('carrier', carrier);
                    fd.set('batch', '10');
                    fd.set('consecutive_errors', consecutiveErrors);
                    if (sweepMode) { fd.set('sweep', '1'); }
                    fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd})
                      .then(function(r){return r.json();})
                      .then(function(j){
                        if (!j || !j.success) {
                            status.textContent = (j && j.data && j.data.message) || '<?php echo esc_js(__('Detailed backfill failed.', 'ai-eshop-optimizer')); ?>';
                            status.style.color = '#b91c1c';
                            document.querySelectorAll('.aieo-ff-db-run').forEach(function(b){ b.disabled = false; });
                            return;
                        }
                        var d = j.data || {};
                        processed += d.processed || 0;
                        arrivedTotal += d.arrived_populated || 0;
                        noC3Total += d.no_c_a3 || 0;
                        untrackableTotal += d.untrackable || 0;
                        consecutiveErrors = d.consecutive_errors || 0;
                        if (d.errors && d.errors.length) { allErrors = allErrors.concat(d.errors); }

                        var pct = initialMissing > 0 ? Math.min(100, Math.round((processed / initialMissing) * 100)) : 100;
                        bar.style.width = pct + '%';
                        status.textContent = '<?php echo esc_js(__('Processed', 'ai-eshop-optimizer')); ?>: ' + processed
                            + ' · <?php echo esc_js(__('arrived_at populated', 'ai-eshop-optimizer')); ?>: ' + arrivedTotal
                            + ' · <?php echo esc_js(__('no C_A3 in history', 'ai-eshop-optimizer')); ?>: ' + noC3Total
                            + (untrackableTotal ? ' · ' + untrackableTotal + ' <?php echo esc_js(__('untrackable', 'ai-eshop-optimizer')); ?>' : '')
                            + (allErrors.length ? ' · ' + allErrors.length + ' <?php echo esc_js(__('errors', 'ai-eshop-optimizer')); ?>' : '');
                        if (allErrors.length) {
                            errors.style.display = 'block';
                            errors.textContent = allErrors.slice(-200).join('\n');
                        }

                        if (d.complete) {
                            if (d.aborted_consecutive) {
                                status.style.color = '#b45309';
                                status.textContent += ' · <?php echo esc_js(__('Stopped: 5 consecutive untrackable/error vouchers. Tick Sweep mode and re-run to march through the rest.', 'ai-eshop-optimizer')); ?>';
                                document.querySelectorAll('.aieo-ff-db-run').forEach(function(b){ b.disabled = false; });
                            } else {
                                status.style.color = '#15803d';
                                status.textContent += ' · <?php echo esc_js(__('Done.', 'ai-eshop-optimizer')); ?>';
                                setTimeout(function(){ location.reload(); }, 800);
                            }
                        } else {
                            setTimeout(tick, 250);
                        }
                      })
                      .catch(function(err){
                        status.textContent = String(err);
                        status.style.color = '#b91c1c';
                        document.querySelectorAll('.aieo-ff-db-run').forEach(function(b){ b.disabled = false; });
                      });
                }
                tick();
            });
        })();
        </script>
        <?php
        echo '</div>';
    }

    
    private function render_customer_paid_backfill_card() {
        $nonce = wp_create_nonce('aieo_fulfillment_admin');
        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $missing = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$table} WHERE customer_shipping_paid IS NULL AND order_id > 0"
        );
        $populated = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$table} WHERE customer_shipping_paid IS NOT NULL"
        );
        echo '<div class="aieo-dmm-card" style="margin-top:18px;" data-aieo-ff-cpb data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('Backfill customer shipping paid', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html__('For every voucher with a missing customer_shipping_paid value, reads $order->get_shipping_total() and writes it. Lights up the AVG PAID column for manual-paste vouchers from no-API carriers (Courier Centre, Stoferno, etc.). Different from "Free-shipping cost — retrospective analysis" — that estimates what the carrier would have cost; this captures what the customer actually paid.', 'ai-eshop-optimizer') . '</p>';
        echo '<p><strong>' . esc_html__('Missing:', 'ai-eshop-optimizer') . '</strong> <span class="aieo-ff-cpb-missing">' . (int) $missing . '</span> &nbsp; <strong>' . esc_html__('Populated:', 'ai-eshop-optimizer') . '</strong> <span class="aieo-ff-cpb-populated">' . (int) $populated . '</span></p>';
        if ($missing > 0) {
            echo '<p><button type="button" class="button button-primary aieo-ff-cpb-run">' . esc_html__('Run backfill', 'ai-eshop-optimizer') . '</button> <span class="aieo-ff-cpb-status" style="margin-left:10px;font-size:12px;color:#646970;"></span></p>';
        } else {
            echo '<p><em>' . esc_html__('All vouchers have a customer_shipping_paid value.', 'ai-eshop-optimizer') . '</em></p>';
        }
        echo '</div>';
        $ajax = admin_url('admin-ajax.php');
        ?>
        <script>
        (function(){
            var root = document.querySelector('[data-aieo-ff-cpb]');
            if (!root) return;
            var btn = root.querySelector('.aieo-ff-cpb-run');
            if (!btn) return;
            var ajax = <?php echo wp_json_encode($ajax); ?>;
            var nonce = root.dataset.nonce;
            var status = root.querySelector('.aieo-ff-cpb-status');
            btn.addEventListener('click', function(){
                btn.disabled = true;
                function tick() {
                    var fd = new FormData();
                    fd.set('action', 'aieo_ff_backfill_customer_paid');
                    fd.set('nonce', nonce);
                    fd.set('batch', '500');
                    fetch(ajax, { method:'POST', credentials:'same-origin', body: fd })
                        .then(function(r){ return r.json(); })
                        .then(function(j){
                            var d = (j && j.data) || {};
                            status.textContent = '<?php echo esc_js(__('Processed', 'ai-eshop-optimizer')); ?>' + ' ' + (d.processed || 0) + ', ' + '<?php echo esc_js(__('updated', 'ai-eshop-optimizer')); ?>' + ' ' + (d.updated || 0) + ', ' + '<?php echo esc_js(__('remaining', 'ai-eshop-optimizer')); ?>' + ' ' + (d.remaining || 0);
                            if (d.complete) {
                                status.style.color = '#15803d';
                                status.textContent += ' · <?php echo esc_js(__('Done.', 'ai-eshop-optimizer')); ?>';
                                setTimeout(function(){ location.reload(); }, 800);
                            } else {
                                setTimeout(tick, 250);
                            }
                        })
                        .catch(function(){
                            status.style.color = '#b91c1c';
                            status.textContent = 'network error';
                            btn.disabled = false;
                        });
                }
                tick();
            });
        })();
        </script>
        <?php
    }

    
    private function render_cost_backfill_card() {
        $nonce    = wp_create_nonce('aieo_fulfillment_admin');
        $couriers = $this->couriers();
        $candidates = array();
        foreach ($couriers as $code => $driver) {
            if ($driver->has_capability('calculate_cost')) {
                $candidates[$code] = $driver->label();
            }
        }

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        echo '<div class="aieo-dmm-card" style="margin-top:18px;" data-aieo-ff-cost-backfill data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('Backfill estimated costs', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html__('Polls the carrier\'s CalculateCost endpoint for every voucher missing a cost and writes the result with cost_source="estimate_current_tariff". Returns today\'s tariff (not the historical billed amount). Re-runnable any time. If the carrier returns "feature not enabled" (Geniki: result code 12) we abort cleanly — fix is on their side.', 'ai-eshop-optimizer') . '</p>';

        if (empty($candidates)) {
            echo '<p><em>' . esc_html__('No carriers advertise CalculateCost.', 'ai-eshop-optimizer') . '</em></p>';
            echo '</div>';
            return;
        }

        echo '<table class="widefat striped" style="max-width:560px;margin-bottom:10px;"><thead><tr>';
        echo '<th>' . esc_html__('Carrier', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:120px;">' . esc_html__('Missing', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:120px;">' . esc_html__('Costed', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:160px;">' . esc_html__('Action', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        foreach ($candidates as $code => $label) {
            $missing = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE carrier=%s AND carrier_cost IS NULL AND status NOT IN ('canceled','returned','untrackable')",
                $code
            ));
            $costed = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE carrier=%s AND carrier_cost IS NOT NULL",
                $code
            ));
            echo '<tr data-carrier="' . esc_attr($code) . '">';
            echo '<td><strong>' . esc_html($label) . '</strong></td>';
            echo '<td><span class="aieo-ff-cb-missing">' . (int) $missing . '</span></td>';
            echo '<td><span class="aieo-ff-cb-costed">' . (int) $costed . '</span></td>';
            echo '<td>';
            if ($missing > 0) {
                echo '<button type="button" class="button button-primary button-small aieo-ff-cb-run">' . esc_html__('Backfill costs', 'ai-eshop-optimizer') . '</button>';
            } else {
                echo '<em>' . esc_html__('all costed', 'ai-eshop-optimizer') . '</em>';
            }
            echo '</td></tr>';
        }
        echo '</tbody></table>';

        echo '<div class="aieo-ff-cb-progress" style="display:none;margin-top:8px;">';
        echo '<div style="background:#e5e7eb;height:14px;border-radius:7px;overflow:hidden;max-width:560px;">';
        echo '<div class="aieo-ff-cb-bar" style="background:#2563eb;height:100%;width:0%;transition:width 0.2s;"></div>';
        echo '</div>';
        echo '<p class="aieo-ff-cb-status" style="font-size:13px;color:#646970;margin:6px 0 0;"></p>';
        echo '<pre class="aieo-ff-cb-errors" style="display:none;max-height:200px;overflow:auto;background:#fef2f2;padding:8px;font-size:11px;border-radius:4px;margin-top:8px;"></pre>';
        echo '</div>';

        ?>
        <script>
        (function(){
            var card = document.querySelector('[data-aieo-ff-cost-backfill]');
            if (!card) return;
            var nonce  = card.getAttribute('data-nonce');
            var bar    = card.querySelector('.aieo-ff-cb-bar');
            var status = card.querySelector('.aieo-ff-cb-status');
            var errors = card.querySelector('.aieo-ff-cb-errors');
            var prog   = card.querySelector('.aieo-ff-cb-progress');
            var ajaxUrl = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';

            card.addEventListener('click', function(e){
                var btn = e.target.closest('.aieo-ff-cb-run'); if (!btn) return;
                var row = btn.closest('tr[data-carrier]'); if (!row) return;
                var carrier = row.getAttribute('data-carrier');

                document.querySelectorAll('.aieo-ff-cb-run').forEach(function(b){ b.disabled = true; });
                prog.style.display = 'block';
                errors.style.display = 'none';
                errors.textContent = '';
                bar.style.width = '0%';

                var processed = 0;
                var costedTotal = 0;
                var allErrors = [];
                var initialMissing = parseInt(row.querySelector('.aieo-ff-cb-missing').textContent, 10) || 0;

                function tick() {
                    var fd = new FormData();
                    fd.set('action', 'aieo_ff_backfill_costs');
                    fd.set('nonce', nonce);
                    fd.set('carrier', carrier);
                    fd.set('batch', '10');
                    fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd})
                      .then(function(r){return r.json();})
                      .then(function(j){
                        if (!j || !j.success) {
                            status.textContent = (j && j.data && j.data.message) || '<?php echo esc_js(__('Cost backfill failed.', 'ai-eshop-optimizer')); ?>';
                            status.style.color = '#b91c1c';
                            document.querySelectorAll('.aieo-ff-cb-run').forEach(function(b){ b.disabled = false; });
                            return;
                        }
                        var d = j.data || {};
                        processed += d.processed || 0;
                        costedTotal += d.costed || 0;
                        if (d.errors && d.errors.length) { allErrors = allErrors.concat(d.errors); }

                        var pct = initialMissing > 0 ? Math.min(100, Math.round((processed / initialMissing) * 100)) : 100;
                        bar.style.width = pct + '%';
                        status.textContent = '<?php echo esc_js(__('Processed', 'ai-eshop-optimizer')); ?>: ' + processed
                            + ' · <?php echo esc_js(__('Costed', 'ai-eshop-optimizer')); ?>: ' + costedTotal
                            + (allErrors.length ? ' · ' + allErrors.length + ' <?php echo esc_js(__('errors', 'ai-eshop-optimizer')); ?>' : '');
                        if (allErrors.length) {
                            errors.style.display = 'block';
                            errors.textContent = allErrors.slice(-200).join('\n');
                        }

                        if (d.complete) {
                            if (d.account_disabled) {
                                status.style.color = '#b45309';
                                status.textContent += ' · <?php echo esc_js(__('Stopped: CalculateCost not enabled on this account. Re-run after the carrier flips it on.', 'ai-eshop-optimizer')); ?>';
                                document.querySelectorAll('.aieo-ff-cb-run').forEach(function(b){ b.disabled = false; });
                            } else {
                                status.style.color = '#15803d';
                                status.textContent += ' · <?php echo esc_js(__('Done.', 'ai-eshop-optimizer')); ?>';
                                setTimeout(function(){ location.reload(); }, 800);
                            }
                        } else {
                            setTimeout(tick, 250);
                        }
                      })
                      .catch(function(err){
                        status.textContent = String(err);
                        status.style.color = '#b91c1c';
                        document.querySelectorAll('.aieo-ff-cb-run').forEach(function(b){ b.disabled = false; });
                      });
                }
                tick();
            });
        })();
        </script>
        <?php
        echo '</div>';
    }

    
    
    private function render_carrier_invoice_import_card() {
        $couriers = $this->couriers();
        if (empty($couriers)) { return; }
        $nonce = wp_create_nonce('aieo_fulfillment_admin');

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $maps     = array();
        $imported = array();
        foreach ($couriers as $code => $driver) {
            $maps[$code]     = $this->invoice_csv_map($code);
            $imported[$code] = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE carrier=%s AND cost_source='invoice_csv'",
                $code
            ));
        }
        $default_carrier = isset($couriers['geniki']) ? 'geniki' : (string) array_key_first($couriers);

        echo '<div class="aieo-dmm-card" style="margin-top:18px;" data-aieo-ff-inv-csv data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('Import carrier invoice CSV (per-courier cost)', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html__('Couriers bill the FINAL per-voucher cost in a month-end invoice file (after volume discounts + surcharges) that no price API returns. Upload that file to write carrier_cost for the matched vouchers — this feeds the Delivery statistics and the cost reports. Choose the carrier, map its CSV columns once (Geniki is pre-configured), then upload. Re-uploading a later file overwrites prior values for the same voucher.', 'ai-eshop-optimizer') . '</p>';

        echo '<p style="margin:8px 0;"><label style="margin-right:6px;"><strong>' . esc_html__('Carrier:', 'ai-eshop-optimizer') . '</strong></label>';
        echo '<select class="aieo-ff-inv-carrier" style="min-width:220px;">';
        foreach ($couriers as $code => $driver) {
            echo '<option value="' . esc_attr($code) . '"' . selected($default_carrier, $code, false) . '>' . esc_html($driver->label()) . '</option>';
        }
        echo '</select> <span class="aieo-ff-inv-imported" style="font-size:12px;color:#646970;margin-left:8px;"></span></p>';

        echo '<details style="margin:6px 0 10px;"><summary style="cursor:pointer;font-weight:600;">' . esc_html__('CSV column mapping', 'ai-eshop-optimizer') . '</summary>';
        echo '<p class="description" style="font-size:12px;">' . esc_html__('Column numbers are 0-based (first column = 0); -1 = absent. Decimal: "comma" for Greek 1.234,56 — "dot" for 1,234.56. Delimiter accepts ; , or the word tab.', 'ai-eshop-optimizer') . '</p>';
        echo '<table class="form-table" style="margin:0;"><tbody>';
        echo '<tr><th>' . esc_html__('Delimiter', 'ai-eshop-optimizer') . '</th><td><input type="text" class="aieo-ff-inv-map" data-k="delimiter" maxlength="3" style="width:60px;"></td></tr>';
        echo '<tr><th>' . esc_html__('Encoding', 'ai-eshop-optimizer') . '</th><td><input type="text" class="aieo-ff-inv-map" data-k="encoding" style="width:160px;"></td></tr>';
        echo '<tr><th>' . esc_html__('Decimal', 'ai-eshop-optimizer') . '</th><td><select class="aieo-ff-inv-map" data-k="decimal"><option value="dot">dot</option><option value="comma">comma</option></select></td></tr>';
        $int_fields = array(
            'voucher_col'     => __('Voucher # column', 'ai-eshop-optimizer'),
            'cost_col'        => __('Total cost column', 'ai-eshop-optimizer'),
            'weight_col'      => __('Weight column', 'ai-eshop-optimizer'),
            'pieces_col'      => __('Pieces column', 'ai-eshop-optimizer'),
            'consignee_col'   => __('Consignee column', 'ai-eshop-optimizer'),
            'invoice_col'     => __('Invoice # column', 'ai-eshop-optimizer'),
            'date_col'        => __('Date column', 'ai-eshop-optimizer'),
            'min_cols'        => __('Min columns per row', 'ai-eshop-optimizer'),
            'voucher_min_len' => __('Min voucher # length', 'ai-eshop-optimizer'),
        );
        foreach ($int_fields as $k => $lbl) {
            echo '<tr><th>' . esc_html($lbl) . '</th><td><input type="number" class="aieo-ff-inv-map" data-k="' . esc_attr($k) . '" style="width:90px;"></td></tr>';
        }
        echo '<tr><th>' . esc_html__('Voucher # is digits-only', 'ai-eshop-optimizer') . '</th><td><input type="checkbox" class="aieo-ff-inv-map-bool" data-k="voucher_digits_only"></td></tr>';
        echo '</tbody></table></details>';

        echo '<div style="display:flex;align-items:center;gap:12px;margin-top:8px;">';
        echo '<input type="file" class="aieo-ff-inv-file" accept=".csv,text/csv,application/vnd.ms-excel" style="max-width:320px;"> ';
        echo '<button type="button" class="button button-primary aieo-ff-inv-upload">' . esc_html__('Upload + reconcile', 'ai-eshop-optimizer') . '</button>';
        echo '</div>';
        echo '<div class="aieo-ff-inv-status" style="display:none;font-size:13px;margin-top:8px;"></div>';
        echo '<pre class="aieo-ff-inv-errors" style="display:none;max-height:200px;overflow:auto;background:#fef2f2;padding:8px;font-size:11px;border-radius:4px;margin-top:8px;"></pre>';

        $json_opts = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
        ?>
        <script>
        (function(){
            var card = document.querySelector('[data-aieo-ff-inv-csv]');
            if (!card) return;
            var nonce   = card.getAttribute('data-nonce');
            var MAPS     = <?php echo wp_json_encode($maps, $json_opts); ?> || {};
            var IMPORTED = <?php echo wp_json_encode($imported, $json_opts); ?> || {};
            var ajaxUrl  = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
            var sel        = card.querySelector('.aieo-ff-inv-carrier');
            var importedEl = card.querySelector('.aieo-ff-inv-imported');
            var fileEl     = card.querySelector('.aieo-ff-inv-file');
            var btn        = card.querySelector('.aieo-ff-inv-upload');
            var status     = card.querySelector('.aieo-ff-inv-status');
            var errors     = card.querySelector('.aieo-ff-inv-errors');

            function fillMap(){
                var m = MAPS[sel.value] || {};
                card.querySelectorAll('.aieo-ff-inv-map').forEach(function(el){
                    var k = el.getAttribute('data-k');
                    if (m[k] !== undefined && m[k] !== null) { el.value = m[k]; }
                });
                card.querySelectorAll('.aieo-ff-inv-map-bool').forEach(function(el){
                    el.checked = !!(+m[el.getAttribute('data-k')]);
                });
                importedEl.textContent = '<?php echo esc_js(__('Already imported:', 'ai-eshop-optimizer')); ?> ' + (IMPORTED[sel.value]||0) + ' <?php echo esc_js(__('voucher(s)', 'ai-eshop-optimizer')); ?>';
            }
            sel.addEventListener('change', fillMap);
            fillMap();

            btn.addEventListener('click', function(){
                if (!fileEl.files || !fileEl.files[0]) {
                    status.style.display='block'; status.style.color='#b91c1c';
                    status.textContent='<?php echo esc_js(__('Pick a file first.', 'ai-eshop-optimizer')); ?>'; return;
                }
                btn.disabled = true;
                status.style.display='block'; status.style.color='#646970';
                status.textContent='<?php echo esc_js(__('Uploading + reconciling…', 'ai-eshop-optimizer')); ?>';
                errors.style.display='none'; errors.textContent='';
                var fd = new FormData();
                fd.set('action','aieo_ff_import_carrier_csv');
                fd.set('nonce', nonce);
                fd.set('carrier', sel.value);
                card.querySelectorAll('.aieo-ff-inv-map').forEach(function(el){ fd.set(el.getAttribute('data-k'), el.value); });
                card.querySelectorAll('.aieo-ff-inv-map-bool').forEach(function(el){ fd.set(el.getAttribute('data-k'), el.checked ? 1 : 0); });
                fd.append('file', fileEl.files[0]);
                fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd})
                  .then(function(r){return r.json();})
                  .then(function(j){
                    btn.disabled = false;
                    if (!j || !j.success) {
                        status.style.color='#b91c1c';
                        status.textContent=(j&&j.data&&j.data.message)||'<?php echo esc_js(__('Import failed.', 'ai-eshop-optimizer')); ?>';
                        if (j&&j.data&&j.data.errors&&j.data.errors.length){ errors.style.display='block'; errors.textContent=j.data.errors.join('\n'); }
                        return;
                    }
                    var d = j.data||{};
                    status.style.color='#15803d';
                    status.innerHTML='<strong>'+(d.matched||0)+'</strong> <?php echo esc_js(__('vouchers matched and costed', 'ai-eshop-optimizer')); ?> (<?php echo esc_js(__('updated', 'ai-eshop-optimizer')); ?>: '+(d.updated||0)+') · '+(d.unmatched||0)+' <?php echo esc_js(__('rows had no matching voucher', 'ai-eshop-optimizer')); ?> · '+(d.parsed_rows||0)+' <?php echo esc_js(__('rows parsed', 'ai-eshop-optimizer')); ?>.';
                    if (d.errors&&d.errors.length){ errors.style.display='block'; errors.textContent=d.errors.join('\n'); }
                    setTimeout(function(){ location.reload(); }, 1800);
                  })
                  .catch(function(err){ btn.disabled=false; status.style.color='#b91c1c'; status.textContent=String(err); });
            });
        })();
        </script>
        <?php
        echo '</div>';
    }

    private function render_geniki_invoice_import_card() {
        $nonce = wp_create_nonce('aieo_fulfillment_admin');
        $couriers = $this->couriers();
        
        
        if (!isset($couriers['geniki'])) { return; }

        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';
        $invoice_costed = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$table}
              WHERE carrier='geniki' AND cost_source='invoice_csv'"
        );

        echo '<div class="aieo-dmm-card" style="margin-top:18px;" data-aieo-ff-geniki-csv data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('Geniki — import monthly invoice CSV', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html(__(
            'Upload the ΤΠΥ-GR-Β.-NNNN.csv file Geniki emails at month-end. ' .
            'It carries the FINAL billed cost per voucher (after volume discounts + ' .
            'island/express surcharges). The CalculateCost API can\'t return these ' .
            'because they\'re computed at month-end. Re-uploading a later file ' .
            'overwrites prior values for the same voucher.',
            'ai-eshop-optimizer'
        )) . '</p>';

        echo '<p style="font-size:13px;color:#646970;margin-top:6px;">';
        printf(
            
            esc_html__('Already imported from prior invoices: %d voucher(s).', 'ai-eshop-optimizer'),
            (int) $invoice_costed
        );
        echo '</p>';

        
        
        
        
        
        
        ?>
        <style>
            .aieo-ff-geniki-csv-controls {
                display: flex !important;
                flex-direction: row !important;
                flex-wrap: nowrap !important;
                align-items: center !important;
                gap: 12px !important;
                margin-top: 8px !important;
                width: auto !important;
                background: transparent !important;
                box-shadow: none !important;
                padding: 0 !important;
                border: 0 !important;
            }
            .aieo-ff-geniki-csv-controls input[type="file"] {
                display: inline-block !important;
                width: auto !important;
                max-width: 320px !important;
                flex: 0 0 auto !important;
                margin: 0 !important;
                padding: 0 !important;
                background: transparent !important;
                box-shadow: none !important;
                border: 0 !important;
            }
            .aieo-ff-geniki-csv-controls .aieo-ff-geniki-csv-upload {
                flex: 0 0 auto !important;
                margin: 0 !important;
            }
        </style>
        <?php
        echo '<div class="aieo-ff-geniki-csv-controls">';
        echo '<input type="file" class="aieo-ff-geniki-csv-file" accept=".csv,text/csv,application/vnd.ms-excel" /> ';
        echo '<button type="button" class="button button-primary aieo-ff-geniki-csv-upload">' . esc_html__('Upload + reconcile', 'ai-eshop-optimizer') . '</button>';
        echo '</div>';

        echo '<div class="aieo-ff-geniki-csv-status" style="display:none;font-size:13px;margin-top:8px;"></div>';
        echo '<pre class="aieo-ff-geniki-csv-errors" style="display:none;max-height:200px;overflow:auto;background:#fef2f2;padding:8px;font-size:11px;border-radius:4px;margin-top:8px;"></pre>';

        ?>
        <script>
        (function(){
            var card = document.querySelector('[data-aieo-ff-geniki-csv]');
            if (!card) return;
            var nonce  = card.getAttribute('data-nonce');
            var input  = card.querySelector('.aieo-ff-geniki-csv-file');
            var btn    = card.querySelector('.aieo-ff-geniki-csv-upload');
            var status = card.querySelector('.aieo-ff-geniki-csv-status');
            var errors = card.querySelector('.aieo-ff-geniki-csv-errors');
            var ajaxUrl = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';

            btn.addEventListener('click', function(){
                if (!input.files || !input.files[0]) {
                    status.style.display = 'block';
                    status.style.color = '#b91c1c';
                    status.textContent = '<?php echo esc_js(__('Pick a file first.', 'ai-eshop-optimizer')); ?>';
                    return;
                }
                btn.disabled = true;
                status.style.display = 'block';
                status.style.color = '#646970';
                status.textContent = '<?php echo esc_js(__('Uploading + reconciling…', 'ai-eshop-optimizer')); ?>';
                errors.style.display = 'none';
                errors.textContent = '';

                var fd = new FormData();
                fd.set('action', 'aieo_ff_import_geniki_csv');
                fd.set('nonce', nonce);
                fd.append('file', input.files[0]);

                fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd})
                  .then(function(r){return r.json();})
                  .then(function(j){
                    btn.disabled = false;
                    if (!j || !j.success) {
                        status.style.color = '#b91c1c';
                        status.textContent = (j && j.data && j.data.message) || '<?php echo esc_js(__('Import failed.', 'ai-eshop-optimizer')); ?>';
                        if (j && j.data && j.data.errors && j.data.errors.length) {
                            errors.style.display = 'block';
                            errors.textContent = j.data.errors.join('\n');
                        }
                        return;
                    }
                    var d = j.data || {};
                    status.style.color = '#15803d';
                    status.innerHTML = '<strong>' + (d.matched || 0) + '</strong> '
                        + '<?php echo esc_js(__('vouchers matched and costed', 'ai-eshop-optimizer')); ?> '
                        + '(<?php echo esc_js(__('updated', 'ai-eshop-optimizer')); ?>: ' + (d.updated || 0) + ') · '
                        + (d.unmatched || 0) + ' <?php echo esc_js(__('CSV rows had no matching voucher in our DB', 'ai-eshop-optimizer')); ?> · '
                        + (d.parsed_rows || 0) + ' <?php echo esc_js(__('rows parsed from file', 'ai-eshop-optimizer')); ?>.';
                    if (d.errors && d.errors.length) {
                        errors.style.display = 'block';
                        errors.textContent = d.errors.join('\n');
                    }
                    setTimeout(function(){ location.reload(); }, 1500);
                  })
                  .catch(function(err){
                    btn.disabled = false;
                    status.style.color = '#b91c1c';
                    status.textContent = String(err);
                  });
            });
        })();
        </script>
        <?php
        echo '</div>';
    }

    
    private function render_orphan_invoice_lines_card() {
        global $wpdb;
        $table = $wpdb->prefix . 'aieo_dmm_fulfillment_vouchers';

        
        
        $rollup = $wpdb->get_results(
            "SELECT carrier,
                    COUNT(*)              AS n,
                    SUM(carrier_cost)     AS total_cost
               FROM {$table}
              WHERE order_id = 0
                AND status   = 'orphan_invoice'
              GROUP BY carrier
              ORDER BY total_cost DESC",
            ARRAY_A
        );

        echo '<div class="aieo-dmm-card" style="margin-top:18px;">';
        echo '<h3>' . esc_html__('Orphan invoice lines (cost without a matching voucher)', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html(__(
            'Lines from monthly carrier invoices that we could not match to ' .
            'any voucher in our database. Common causes: a voucher generated ' .
            'outside the AIEO flow, a voucher created before the fulfillment ' .
            'module existed, a voucher belonging to a different store account ' .
            'under the same carrier login, or a typo on the carrier\'s side. ' .
            'These rows still carry a real billed cost — kept here so the ' .
            'monthly total reconciles to the carrier\'s invoice total.',
            'ai-eshop-optimizer'
        )) . '</p>';

        if (empty($rollup)) {
            echo '<p><em>' . esc_html__('No orphan invoice lines on file.', 'ai-eshop-optimizer') . '</em></p>';
            echo '</div>';
            return;
        }

        echo '<table class="widefat striped" style="max-width:560px;margin-bottom:14px;"><thead><tr>';
        echo '<th>' . esc_html__('Carrier', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:120px;text-align:right;">' . esc_html__('Orphans', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:160px;text-align:right;">' . esc_html__('Cost (€)', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        $grand_n = 0;
        $grand_c = 0.0;
        foreach ($rollup as $r) {
            $couriers = $this->couriers();
            $label = isset($couriers[$r['carrier']])
                ? $couriers[$r['carrier']]->label()
                : (string) $r['carrier'];
            echo '<tr>';
            echo '<td><strong>' . esc_html($label) . '</strong></td>';
            echo '<td style="text-align:right;">' . esc_html(number_format_i18n((int) $r['n'])) . '</td>';
            echo '<td style="text-align:right;">' . esc_html(number_format_i18n((float) $r['total_cost'], 2)) . '</td>';
            echo '</tr>';
            $grand_n += (int) $r['n'];
            $grand_c += (float) $r['total_cost'];
        }
        echo '</tbody>';
        if (count($rollup) > 1) {
            echo '<tfoot><tr style="font-weight:600;background:#f6f7f7;">';
            echo '<td>' . esc_html__('Total', 'ai-eshop-optimizer') . '</td>';
            echo '<td style="text-align:right;">' . esc_html(number_format_i18n($grand_n)) . '</td>';
            echo '<td style="text-align:right;">' . esc_html(number_format_i18n($grand_c, 2)) . '</td>';
            echo '</tr></tfoot>';
        }
        echo '</table>';

        
        
        
        
        
        
        
        
        $details = $wpdb->get_results(
            "SELECT carrier, voucher_no, carrier_cost,
                    consignee, postal_code,
                    reference_1 AS invoice_no, reference_2 AS pickup_date,
                    pieces, weight, created_at
               FROM {$table}
              WHERE order_id = 0
                AND status   = 'orphan_invoice'
              ORDER BY carrier_cost DESC, voucher_no
              LIMIT 50",
            ARRAY_A
        );

        echo '<details><summary style="cursor:pointer;font-weight:600;font-size:13px;">'
            . esc_html(sprintf(
                
                __('Top %d orphans by cost', 'ai-eshop-optimizer'),
                count($details)
            ))
            . '</summary>';
        echo '<table class="widefat striped" style="margin-top:8px;font-size:12px;"><thead><tr>';
        echo '<th>' . esc_html__('Carrier', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Voucher', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="text-align:right;">' . esc_html__('Cost (€)', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Consignee', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('TK', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Invoice #', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Pickup date', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="text-align:right;">' . esc_html__('Pieces', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="text-align:right;">' . esc_html__('Weight (kg)', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Imported', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        foreach ($details as $r) {
            echo '<tr>';
            echo '<td>' . esc_html((string) $r['carrier']) . '</td>';
            echo '<td><code>' . esc_html((string) $r['voucher_no']) . '</code></td>';
            echo '<td style="text-align:right;">' . esc_html(number_format_i18n((float) $r['carrier_cost'], 2)) . '</td>';
            echo '<td>' . esc_html((string) $r['consignee']) . '</td>';
            echo '<td>' . esc_html($r['postal_code'] === null || $r['postal_code'] === '' ? '—' : (string) $r['postal_code']) . '</td>';
            echo '<td>' . esc_html((string) $r['invoice_no']) . '</td>';
            echo '<td>' . esc_html((string) $r['pickup_date']) . '</td>';
            echo '<td style="text-align:right;">' . esc_html((string) (int) $r['pieces']) . '</td>';
            echo '<td style="text-align:right;">' . esc_html($r['weight'] === null ? '—' : number_format_i18n((float) $r['weight'], 3)) . '</td>';
            echo '<td>' . esc_html((string) $r['created_at']) . '</td>';
            echo '</tr>';
        }
        echo '</tbody></table>';
        echo '</details>';
        echo '</div>';
    }

    private function render_migration_card() {
        if (!class_exists('AIEO_Fulfillment_Migrator')) { return; }
        $nonce = wp_create_nonce('aieo_fulfillment_admin');
        $steps = AIEO_Fulfillment_Migrator::steps();
        $progress = AIEO_Fulfillment_Migrator::progress();

        echo '<div class="aieo-dmm-card" style="margin-top:18px;" data-aieo-ff-migrate data-nonce="' . esc_attr($nonce) . '">';
        echo '<h3>' . esc_html__('Legacy data migration', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description">' . esc_html__('Permanent, idempotent, re-runnable tooling. Lifts data from the legacy plugins (WPO PDF Invoices, WC Shipment Tracking, ACS Voucher) into our own tables. Safe on satellite stores and clones — re-running never duplicates.', 'ai-eshop-optimizer') . '</p>';

        echo '<p><button type="button" class="button" id="aieo-ff-migrate-audit">' . esc_html__('Audit (read-only)', 'ai-eshop-optimizer') . '</button> ';
        echo '<button type="button" class="button button-link-delete" id="aieo-ff-migrate-reset">' . esc_html__('Reset progress', 'ai-eshop-optimizer') . '</button> ';
        echo '<span id="aieo-ff-migrate-audit-out" style="margin-left:10px;font-size:13px;color:#646970;"></span></p>';

        echo '<table class="widefat striped" id="aieo-ff-migrate-table" style="margin-top:8px;"><thead><tr>';
        echo '<th style="width:42%;">' . esc_html__('Step', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:14%;">' . esc_html__('Created', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:14%;">' . esc_html__('Skipped', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:18%;">' . esc_html__('Progress', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:12%;">' . esc_html__('Action', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        foreach ($steps as $key => $label) {
            $p = isset($progress[$key]) && is_array($progress[$key]) ? $progress[$key] : array();
            $created = (int) ($p['created'] ?? 0);
            $skipped = (int) ($p['skipped'] ?? 0);
            $total   = (int) ($p['total']   ?? 0);
            $done    = !empty($p['done']);
            $proc    = $created + $skipped;
            $pct     = ($total > 0) ? min(100, (int) floor(100 * $proc / $total)) : ($done ? 100 : 0);
            echo '<tr data-step="' . esc_attr($key) . '">';
            echo '<td>' . esc_html($label) . '</td>';
            echo '<td class="aieo-ff-migrate-created">' . esc_html((string) $created) . '</td>';
            echo '<td class="aieo-ff-migrate-skipped">' . esc_html((string) $skipped) . '</td>';
            echo '<td><div style="background:#e5e7eb;height:14px;border-radius:7px;overflow:hidden;"><div class="aieo-ff-migrate-bar" style="background:#15803d;height:100%;width:' . esc_attr((string) $pct) . '%;"></div></div>';
            echo '<small class="aieo-ff-migrate-counter" style="font-size:11px;color:#6b7280;">' . esc_html(sprintf('%d / %d', $proc, $total)) . '</small></td>';
            echo '<td><button type="button" class="button button-primary aieo-ff-migrate-run">' . esc_html($done ? __('Re-run', 'ai-eshop-optimizer') : __('Run', 'ai-eshop-optimizer')) . '</button></td>';
            echo '</tr>';
        }
        echo '</tbody></table>';
        echo '<p><span id="aieo-ff-migrate-status" style="font-size:13px;color:#646970;"></span></p>';

        echo '<details style="margin-top:14px;"><summary>' . esc_html__('Advanced — batch size', 'ai-eshop-optimizer') . '</summary>';
        echo '<p>' . esc_html__('Rows per batch (50–5000):', 'ai-eshop-optimizer') . ' <input type="number" id="aieo-ff-migrate-batch-size" value="500" min="50" max="5000" style="width:80px;"></p>';
        echo '<p class="description">' . esc_html__('Larger batches finish faster but use more memory per request. 500 is safe at default memory limits.', 'ai-eshop-optimizer') . '</p>';
        echo '</details>';

        echo '</div>';

        $this->print_migration_js($nonce);
    }

    private function print_migration_js($nonce) {
        $ajax = admin_url('admin-ajax.php');
        ?>
        <script>
        (function(){
            var box = document.querySelector('[data-aieo-ff-migrate]'); if (!box) return;
            var nonce  = '<?php echo esc_js($nonce); ?>';
            var ajaxUrl = '<?php echo esc_js($ajax); ?>';
            function setStatus(id, color, text){ var el = document.getElementById(id); if (!el) return; el.style.color = color; el.textContent = text; }
            function post(action, data){
                var fd = new FormData();
                fd.set('action', action); fd.set('nonce', nonce);
                Object.keys(data || {}).forEach(function(k){ fd.set(k, data[k]); });
                return fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body: fd}).then(function(r){return r.json();});
            }
            function batchSize(){ return parseInt((document.getElementById('aieo-ff-migrate-batch-size')||{}).value || 500, 10) || 500; }
            function updateRow(step, data){
                var row = box.querySelector('tr[data-step="' + step + '"]'); if (!row) return;
                var processed = (data.created|0) + (data.skipped|0);
                var total = data.total|0;
                var pct = total > 0 ? Math.min(100, Math.floor(100 * processed / total)) : (data.done ? 100 : 0);
                row.querySelector('.aieo-ff-migrate-created').textContent = (data.created|0);
                row.querySelector('.aieo-ff-migrate-skipped').textContent = (data.skipped|0);
                row.querySelector('.aieo-ff-migrate-bar').style.width = pct + '%';
                row.querySelector('.aieo-ff-migrate-counter').textContent = processed + ' / ' + total;
            }

            // Audit
            var auditBtn = document.getElementById('aieo-ff-migrate-audit');
            auditBtn && auditBtn.addEventListener('click', function(){
                auditBtn.disabled = true;
                setStatus('aieo-ff-migrate-audit-out', '#646970', '<?php echo esc_js(__('Auditing…', 'ai-eshop-optimizer')); ?>');
                post('aieo_ff_migrate_audit', {}).then(function(j){
                    auditBtn.disabled = false;
                    if (!j || !j.success) { setStatus('aieo-ff-migrate-audit-out', '#b91c1c', '<?php echo esc_js(__('Audit failed.', 'ai-eshop-optimizer')); ?>'); return; }
                    var d = j.data;
                    setStatus('aieo-ff-migrate-audit-out', '#15803d',
                        '<?php echo esc_js(__('Tracking:', 'ai-eshop-optimizer')); ?> ' + (d.tracking_orders|0) + ' · ' +
                        '<?php echo esc_js(__('Invoices:', 'ai-eshop-optimizer')); ?> ' + (d.wpdf_invoice|0) + ' · ' +
                        '<?php echo esc_js(__('ACS legacy:', 'ai-eshop-optimizer')); ?> ' + (d.acs_legacy|0) + ' · ' +
                        '<?php echo esc_js(__('Already migrated vouchers:', 'ai-eshop-optimizer')); ?> ' + (d.destination_vouchers|0));
                });
            });

            // Reset progress
            var resetBtn = document.getElementById('aieo-ff-migrate-reset');
            resetBtn && resetBtn.addEventListener('click', function(){
                if (!confirm('<?php echo esc_js(__('Reset migration progress counters? Already-migrated rows stay (idempotency keys block dupes), but counters restart from zero.', 'ai-eshop-optimizer')); ?>')) return;
                resetBtn.disabled = true;
                post('aieo_ff_migrate_reset', {}).then(function(){
                    location.reload();
                });
            });

            // Per-row Run — loops until done
            box.addEventListener('click', function(e){
                var btn = e.target.closest('.aieo-ff-migrate-run'); if (!btn) return;
                var row = btn.closest('tr[data-step]'); if (!row) return;
                var step = row.getAttribute('data-step');
                btn.disabled = true;
                var stop = false;
                function tick(){
                    if (stop) return;
                    setStatus('aieo-ff-migrate-status', '#646970', '<?php echo esc_js(__('Working on', 'ai-eshop-optimizer')); ?> ' + step + '…');
                    post('aieo_ff_migrate_batch', {step: step, batch_size: batchSize()}).then(function(j){
                        if (!j || !j.success) {
                            btn.disabled = false; stop = true;
                            setStatus('aieo-ff-migrate-status', '#b91c1c', (j && j.data && j.data.message) || '<?php echo esc_js(__('Batch failed.', 'ai-eshop-optimizer')); ?>');
                            return;
                        }
                        var d = j.data;
                        updateRow(step, d);
                        if (d.done) {
                            btn.disabled = false;
                            setStatus('aieo-ff-migrate-status', '#15803d', '<?php echo esc_js(__('Done.', 'ai-eshop-optimizer')); ?> ' + step + ' — ' + ((d.created|0) + ' ' + '<?php echo esc_js(__('created', 'ai-eshop-optimizer')); ?>') + ', ' + ((d.skipped|0) + ' ' + '<?php echo esc_js(__('skipped', 'ai-eshop-optimizer')); ?>'));
                            return;
                        }
                        // schedule next batch — yield to UI thread
                        setTimeout(tick, 30);
                    });
                }
                tick();
            });
        })();
        </script>
        <?php
    }
}
