<?php
/**
 * almacen.php
 * ============================================================
 * BReport · MÓDULO ALMACÉN
 * ------------------------------------------------------------
 * Regla operativa:
 * - Barra disponible comercial = CF1 + CF2
 * - CUB / SOB-CUB / cualquier almacén interno NO cuenta como
 *   inventario comercial disponible
 * - Producto terminado = cubicado terminado
 * - Transferencias internas solo mueven ubicación
 * - Inventario inicial operativo = saldo comercial al corte 06:59 AM del día seleccionado
 *
 * Integración ventas:
 * - Ver solicitudes AUTORIZADA de sales_orders
 * - Consultar detalle
 * - Generar salida de almacén desde pedido autorizado
 * - Crear outbounds / outbound_items / inventory_moves
 * - Actualizar sales_orders.status => SURTIENDO
 * - Guardar sales_orders.outbound_id
 * ============================================================
 */

declare(strict_types=1);

/*
 * Zona horaria operativa BReport.
 * Reynosa, Tamaulipas usa el identificador IANA America/Matamoros.
 * Esto evita desfases cuando el hosting/VPS está en UTC u otra zona.
 */
const BREPORT_TIMEZONE = 'America/Matamoros';
date_default_timezone_set(BREPORT_TIMEZONE);


require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/helpers.php';
require_once __DIR__ . '/includes/csrf.php';

require_scope();
ensure_session();

$u   = current_user();
$pdo = pdo();
breport_configure_db_timezone($pdo);

$scope = scope();
$companyId  = (int)$scope['company_id'];
$locationId = (int)$scope['location_id'];

$scopeCompanyName = '';
$scopeLocationName = '';
try {
    $stScope = $pdo->prepare("SELECT name FROM companies WHERE id=? LIMIT 1");
    $stScope->execute([$companyId]);
    $scopeCompanyName = (string)($stScope->fetchColumn() ?: ('Empresa #' . $companyId));

    $stScope = $pdo->prepare("SELECT name FROM locations WHERE id=? AND company_id=? LIMIT 1");
    $stScope->execute([$locationId, $companyId]);
    $scopeLocationName = (string)($stScope->fetchColumn() ?: ('Localidad #' . $locationId));
} catch (Throwable $e) {
    $scopeCompanyName = 'Empresa #' . $companyId;
    $scopeLocationName = 'Localidad #' . $locationId;
}

/*
 * Alcance funcional por planta:
 * Hielo Barra conserva integraciones y ayudas específicas de barra/VentasHielo.
 * El resto de plantas trabajan como MES general por almacenes activos de su localidad.
 */
$scopePlantText = mb_strtoupper(trim($scopeCompanyName . ' ' . $scopeLocationName), 'UTF-8');

/*
 * MÓDULO EXCLUSIVO BOTELLA
 * Botella trabaja con almacenes configurados por empresa/localidad y no hereda
 * reglas CF1/CF2/CF3, cubicado ni integración VentasHielo de Hielo Barra.
 */
$isBotellaScope = str_contains($scopePlantText, 'BOTELLA');
if (!$isBotellaScope) {
    http_response_code(403);
    exit('Esta sección de Almacén corresponde únicamente a Botella. Selecciona la empresa/planta Botella desde Dashboard.');
}
$isHieloBarraScope = false;
$scopeWarehouseHelpText = 'Inventario Botella: saldo inicial al corte de las 07:00 AM, entradas, salidas e inventario actual solo del almacén principal ALM01. Las rutas se revisan por separado como almacenes móviles.';

$canWrite = has_role('admin') || has_role('warehouse') || has_role('almacen');

/*
 * Entrada temporal de existencias por falta de módulo de Producción:
 * - Se habilita únicamente para Botella, Hielo Bolsa y Garrafón.
 * - Hielo Barra queda excluida para no intervenir su flujo funcional
 *   de Producción / Cubicado / reglas CF1-CF2-CF3.
 */
$isManualExistenceScope = (
    !$isHieloBarraScope
    && (
        str_contains($scopePlantText, 'BOTELLA')
        || (str_contains($scopePlantText, 'HIELO') && str_contains($scopePlantText, 'BOLSA'))
        || str_contains($scopePlantText, 'GARRAF')
    )
);
$canAddExistence = $canWrite && $isManualExistenceScope;

$action   = (string)($_GET['action'] ?? '');

ensure_warehouse_merma_schema($pdo);
ensure_location_request_schema($pdo);
ensure_default_merma_concepts($pdo, $companyId);
ensure_botella_customer_schema($pdo);
ensure_botella_invoice_schema($pdo);
ensure_botella_forklift_schema($pdo);

/*
 * Botella no utiliza endpoints ni salidas de VentasHielo.
 * Sus pedidos autorizados provienen únicamente de Ventas Botella.
 */

function breport_is_hielo_barra_scope(): bool {
    return (bool)($GLOBALS['isHieloBarraScope'] ?? false);
}

function breport_is_manual_existence_scope(): bool {
    return (bool)($GLOBALS['isManualExistenceScope'] ?? false);
}

function breport_can_add_existence(): bool {
    return (bool)($GLOBALS['canAddExistence'] ?? false);
}

/* ============================================================
   HELPERS
============================================================ */

function breport_timezone_offset(): string {
    $dt = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE));
    $offset = $dt->getOffset();
    $sign = $offset >= 0 ? '+' : '-';
    $offset = abs($offset);
    $hours = intdiv($offset, 3600);
    $minutes = intdiv($offset % 3600, 60);
    return sprintf('%s%02d:%02d', $sign, $hours, $minutes);
}

function breport_configure_db_timezone(PDO $pdo): void {
    /*
     * MySQL/MariaDB NOW(), CURDATE() y CURRENT_TIMESTAMP dependen de la
     * zona horaria de la sesión. Se fija por offset dinámico para que funcione
     * aunque el hosting no tenga cargadas las tablas timezone de MySQL.
     */
    try {
        $pdo->exec("SET time_zone = " . $pdo->quote(breport_timezone_offset()));
    } catch (Throwable $e) {
        // No se detiene el módulo si el hosting bloquea SET time_zone.
        // PHP seguirá usando America/Matamoros.
    }
}

function breport_now(): string {
    return (new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE)))->format('Y-m-d H:i:s');
}

function breport_today(): string {
    return (new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE)))->format('Y-m-d');
}

function breport_operational_start_hour(): int {
    return 7;
}

/**
 * Fecha operativa BReport:
 * - De 07:00:00 a 23:59:59 pertenece al día calendario actual.
 * - De 00:00:00 a 06:59:59 pertenece al día operativo anterior.
 */
function breport_operational_today(): string {
    $now = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE));
    if ((int)$now->format('H') < breport_operational_start_hour()) {
        $now = $now->modify('-1 day');
    }
    return $now->format('Y-m-d');
}

function breport_operational_shift(): int {
    $now = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE));
    $hour = (int)$now->format('H');
    if ($hour >= 7 && $hour < 15) return 1;
    if ($hour >= 15 && $hour < 22) return 2;
    return 3;
}

function breport_operational_window(string $workDate): array {
    $tz = new DateTimeZone(BREPORT_TIMEZONE);
    $start = new DateTimeImmutable($workDate . ' 07:00:00', $tz);
    $end = $start->modify('+1 day');
    return [
        'start' => $start->format('Y-m-d H:i:s'),
        'end' => $end->format('Y-m-d H:i:s'),
        'end_display' => $end->modify('-1 second')->format('Y-m-d H:i:s'),
    ];
}

/**
 * Si el usuario deja la fecha por defecto del calendario y captura antes de las 7 AM,
 * se corrige automáticamente a la fecha operativa anterior. Si el usuario captura una
 * fecha histórica distinta, se respeta esa fecha manual.
 */
function breport_resolve_work_date(string $submittedDate): string {
    $submittedDate = trim($submittedDate);
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $submittedDate)) {
        return breport_operational_today();
    }
    $now = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE));
    $calendarToday = $now->format('Y-m-d');
    if ((int)$now->format('H') < breport_operational_start_hour() && $submittedDate === $calendarToday) {
        return breport_operational_today();
    }
    return $submittedDate;
}

function breport_move_timestamp_expr(string $alias = 'im'): string {
    return "COALESCE({$alias}.created_at, CONCAT({$alias}.work_date, ' 12:00:00'))";
}

function jexit(array $data, int $code = 200): void {
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function read_json(): array {
    $raw = file_get_contents('php://input');
    $arr = json_decode($raw ?: '[]', true);
    return is_array($arr) ? $arr : [];
}

function need_csrf(): void {
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') return;
    $hdr = (string)($_SERVER['HTTP_X_CSRF'] ?? '');
    if ($hdr === '') {
        jexit(['ok' => false, 'error' => 'Falta encabezado X-CSRF'], 419);
    }
    csrf_verify($hdr);
}

function arr_get(array $a, string $k, $default = null) {
    return array_key_exists($k, $a) ? $a[$k] : $default;
}

function column_exists(PDO $pdo, string $table, string $column): bool {
    $st = $pdo->prepare("
        SELECT COUNT(*)
        FROM information_schema.columns
        WHERE table_schema = DATABASE()
          AND table_name = ?
          AND column_name = ?
    ");
    $st->execute([$table, $column]);
    return (int)$st->fetchColumn() > 0;
}


function table_exists(PDO $pdo, string $table): bool {
    $st = $pdo->prepare("\n        SELECT COUNT(*)\n        FROM information_schema.tables\n        WHERE table_schema = DATABASE()\n          AND table_name = ?\n    ");
    $st->execute([$table]);
    return (int)$st->fetchColumn() > 0;
}


function ensure_botella_customer_schema(PDO $pdo): void {
    /*
     * Refuerzo Botella:
     * - customer_code = número manual del cliente del sistema principal.
     * - sales_customer_addresses.location_id = permite puntos de entrega por localidad.
     * - sales_orders.address_id ya se usa desde Ventas Botella y queda disponible
     *   para pedidos/salidas que requieran trazabilidad del punto de entrega.
     */
    try {
        if (table_exists($pdo, 'sales_customers') && !column_exists($pdo, 'sales_customers', 'customer_code')) {
            $pdo->exec("ALTER TABLE sales_customers ADD COLUMN customer_code VARCHAR(50) DEFAULT NULL AFTER legacy_user_id");
        }
    } catch (Throwable $e) {}

    try {
        if (table_exists($pdo, 'sales_customers') && !column_exists($pdo, 'sales_customers', 'location_id')) {
            $pdo->exec("ALTER TABLE sales_customers ADD COLUMN location_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER company_id");
        }
    } catch (Throwable $e) {}

    try {
        if (table_exists($pdo, 'sales_customer_addresses') && !column_exists($pdo, 'sales_customer_addresses', 'location_id')) {
            $pdo->exec("ALTER TABLE sales_customer_addresses ADD COLUMN location_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER customer_id");
        }
    } catch (Throwable $e) {}

    try {
        if (table_exists($pdo, 'sales_orders') && !column_exists($pdo, 'sales_orders', 'address_id')) {
            $pdo->exec("ALTER TABLE sales_orders ADD COLUMN address_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER customer_user_id");
        }
    } catch (Throwable $e) {}

    try {
        if (table_exists($pdo, 'sales_order_items')) {
            if (!column_exists($pdo, 'sales_order_items', 'packaging_option_id')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN packaging_option_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER product_id");
            }
            if (!column_exists($pdo, 'sales_order_items', 'packages_qty')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN packages_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER qty");
            }
            if (!column_exists($pdo, 'sales_order_items', 'pallets_qty')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN pallets_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER packages_qty");
            }
            if (!column_exists($pdo, 'sales_order_items', 'pallet_type')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN pallet_type VARCHAR(40) DEFAULT NULL AFTER pallets_qty");
            }
            if (!column_exists($pdo, 'sales_order_items', 'pallet_specification')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN pallet_specification VARCHAR(60) DEFAULT NULL AFTER pallet_type");
            }
            // Botella OSV: trazabilidad por OC/cliente/dirección por renglón. Para OSA quedan NULL.
            if (!column_exists($pdo, 'sales_order_items', 'purchase_order')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN purchase_order VARCHAR(80) DEFAULT NULL AFTER product_id");
            }
            if (!column_exists($pdo, 'sales_order_items', 'item_customer_id')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN item_customer_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER purchase_order");
            }
            if (!column_exists($pdo, 'sales_order_items', 'item_address_id')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN item_address_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER item_customer_id");
            }
            if (!column_exists($pdo, 'sales_order_items', 'customer_label')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN customer_label VARCHAR(180) DEFAULT NULL AFTER item_address_id");
            }
            if (!column_exists($pdo, 'sales_order_items', 'delivery_address')) {
                $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN delivery_address VARCHAR(255) DEFAULT NULL AFTER customer_label");
            }
        }
    } catch (Throwable $e) {}
}



function ensure_botella_invoice_schema(PDO $pdo): void {
    try {
        if (!table_exists($pdo, 'sales_order_invoices')) {
            $pdo->exec("CREATE TABLE sales_order_invoices (
                id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
                company_id BIGINT UNSIGNED NOT NULL,
                location_id BIGINT UNSIGNED NOT NULL,
                order_id BIGINT UNSIGNED NOT NULL,
                outbound_id BIGINT UNSIGNED DEFAULT NULL,
                invoice_number VARCHAR(80) NOT NULL,
                original_name VARCHAR(180) DEFAULT NULL,
                stored_name VARCHAR(180) DEFAULT NULL,
                file_path VARCHAR(255) DEFAULT NULL,
                mime_type VARCHAR(120) DEFAULT NULL,
                file_size BIGINT UNSIGNED DEFAULT NULL,
                uploaded_by BIGINT UNSIGNED DEFAULT NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY(id),
                KEY idx_soi_order(order_id),
                KEY idx_soi_scope(company_id, location_id),
                KEY idx_soi_invoice(invoice_number)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
        } else {
            add_column_if_missing($pdo, 'sales_order_invoices', 'outbound_id', "outbound_id BIGINT UNSIGNED DEFAULT NULL AFTER order_id");
            add_column_if_missing($pdo, 'sales_order_invoices', 'invoice_number', "invoice_number VARCHAR(80) NOT NULL DEFAULT '' AFTER outbound_id");
            add_column_if_missing($pdo, 'sales_order_invoices', 'original_name', "original_name VARCHAR(180) DEFAULT NULL AFTER invoice_number");
            add_column_if_missing($pdo, 'sales_order_invoices', 'stored_name', "stored_name VARCHAR(180) DEFAULT NULL AFTER original_name");
            add_column_if_missing($pdo, 'sales_order_invoices', 'file_path', "file_path VARCHAR(255) DEFAULT NULL AFTER stored_name");
            add_column_if_missing($pdo, 'sales_order_invoices', 'mime_type', "mime_type VARCHAR(120) DEFAULT NULL AFTER file_path");
            add_column_if_missing($pdo, 'sales_order_invoices', 'file_size', "file_size BIGINT UNSIGNED DEFAULT NULL AFTER mime_type");
            add_column_if_missing($pdo, 'sales_order_invoices', 'uploaded_by', "uploaded_by BIGINT UNSIGNED DEFAULT NULL AFTER file_size");
            add_column_if_missing($pdo, 'sales_order_invoices', 'created_at', "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER uploaded_by");
        }
    } catch (Throwable $e) {}
}

function botella_allowed_upload_ext(): array {
    return ['pdf','xml','jpg','jpeg','png','webp'];
}

function botella_safe_upload_name(string $value): string {
    $value = preg_replace('/[^A-Za-z0-9_\-]/', '_', $value);
    $value = trim((string)$value, '_');
    return $value !== '' ? $value : 'archivo';
}

function pdf_text(?string $s): string {
    if ($s === null) return '';
    $conv = @iconv('UTF-8', 'ISO-8859-1//TRANSLIT', (string)$s);
    return $conv !== false ? $conv : (string)$s;
}

function safe_pdf_filename(string $name): string {
    $name = preg_replace('/[^A-Za-z0-9_\-]/', '_', $name);
    $name = trim((string)$name, '_');
    return $name !== '' ? $name : 'reporte';
}

function build_sales_dispatch_pdf_url(int $orderId, string $folio = ''): string {
    $url = './orden_salida_botella_pdf.php?formato=carta&order_id=' . $orderId;
    if ($folio !== '') {
        $url .= '&folio=' . rawurlencode($folio);
    }
    return $url;
}

/**
 * Obtiene la conexión PDO de VentasHielo sin depender de un solo nombre.
 * El archivo includes/db_ventashielo.php ha existido con diferentes nombres
 * de variable/función en distintas versiones del proyecto.
 */
function ventashielo_pdo_optional(): ?PDO {
    $candidateFunctions = [
        'pdo_ventashielo',
        'ventashielo_pdo',
        'get_ventashielo_pdo',
        'pdoVentasHielo',
        'ventasHieloPdo',
    ];

    foreach ($candidateFunctions as $fn) {
        if (function_exists($fn)) {
            try {
                $conn = $fn();
                if ($conn instanceof PDO) return $conn;
            } catch (Throwable $e) {
                // Se intenta con el siguiente candidato.
            }
        }
    }

    $candidateGlobals = [
        'pdoVentasHielo',
        'pdoVentashielo',
        'pdo_ventashielo',
        'ventasHieloPdo',
        'ventashieloPdo',
        'vhPdo',
        'pdoVH',
        'connVentasHielo',
        'connVentashielo',
    ];

    foreach ($candidateGlobals as $name) {
        if (isset($GLOBALS[$name]) && $GLOBALS[$name] instanceof PDO) {
            return $GLOBALS[$name];
        }
    }

    return null;
}

function normalize_sales_folio(array $row, string $fallbackPrefix = 'PW'): string {
    $salesOrder = trim((string)($row['sales_order'] ?? $row['sales_order_ref'] ?? ''));
    if ($salesOrder !== '') return $salesOrder;

    $ovSystem = trim((string)($row['ov_system'] ?? ''));
    if ($ovSystem !== '') return $ovSystem;

    $id = (int)($row['id'] ?? 0);
    return $id > 0 ? ($fallbackPrefix . '-' . $id) : $fallbackPrefix;
}


function ensure_warehouse_merma_schema(PDO $pdo): void {
    if (!table_exists($pdo, 'warehouse_merma_entries')) {
        $pdo->exec("\n            CREATE TABLE warehouse_merma_entries (\n                id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,\n                company_id BIGINT(20) UNSIGNED NOT NULL,\n                location_id BIGINT(20) UNSIGNED NOT NULL,\n                work_date DATE NOT NULL,\n                product_id BIGINT(20) UNSIGNED NOT NULL,\n                warehouse_id BIGINT(20) UNSIGNED NOT NULL,\n                concept_id BIGINT(20) UNSIGNED NOT NULL,\n                qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,\n                notes VARCHAR(255) DEFAULT NULL,\n                quality_entry_id BIGINT(20) UNSIGNED DEFAULT NULL,\n                inventory_move_id BIGINT(20) UNSIGNED DEFAULT NULL,\n                created_by BIGINT(20) UNSIGNED NOT NULL,\n                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                updated_by BIGINT(20) UNSIGNED DEFAULT NULL,\n                updated_at DATETIME DEFAULT NULL,\n                PRIMARY KEY (id),\n                KEY idx_wme_scope_date (company_id, location_id, work_date),\n                KEY idx_wme_prod_wh (company_id, location_id, product_id, warehouse_id),\n                KEY idx_wme_qe (quality_entry_id),\n                KEY idx_wme_im (inventory_move_id)\n            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci\n        ");
    }
}

function add_column_if_missing(PDO $pdo, string $table, string $column, string $definition): void {
    if (!column_exists($pdo, $table, $column)) {
        $pdo->exec("ALTER TABLE `{$table}` ADD COLUMN {$definition}");
    }
}

function add_index_if_missing(PDO $pdo, string $table, string $indexName, string $definition): void {
    $st = $pdo->prepare("\n        SELECT COUNT(*)\n        FROM information_schema.statistics\n        WHERE table_schema = DATABASE()\n          AND table_name = ?\n          AND index_name = ?\n    ");
    $st->execute([$table, $indexName]);
    if ((int)$st->fetchColumn() === 0) {
        $pdo->exec("ALTER TABLE `{$table}` ADD {$definition}");
    }
}

function ensure_location_request_schema(PDO $pdo): void {
    if (!table_exists($pdo, 'warehouse_location_requests')) {
        $pdo->exec("\n            CREATE TABLE warehouse_location_requests (\n                id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,\n                company_id BIGINT(20) UNSIGNED NOT NULL,\n                request_location_id BIGINT(20) UNSIGNED NOT NULL,\n                source_location_id BIGINT(20) UNSIGNED NOT NULL,\n                request_date DATE NOT NULL,\n                required_date DATE DEFAULT NULL,\n                status VARCHAR(30) NOT NULL DEFAULT 'PENDIENTE',\n                requested_by BIGINT(20) UNSIGNED NOT NULL,\n                approved_by BIGINT(20) UNSIGNED DEFAULT NULL,\n                approved_at DATETIME DEFAULT NULL,\n                rejected_by BIGINT(20) UNSIGNED DEFAULT NULL,\n                rejected_at DATETIME DEFAULT NULL,\n                reject_reason VARCHAR(255) DEFAULT NULL,\n                shipped_by BIGINT(20) UNSIGNED DEFAULT NULL,\n                shipped_at DATETIME DEFAULT NULL,\n                received_by BIGINT(20) UNSIGNED DEFAULT NULL,\n                received_at DATETIME DEFAULT NULL,\n                driver_name VARCHAR(120) DEFAULT NULL,\n                vehicle_unit VARCHAR(80) DEFAULT NULL,\n                notes VARCHAR(255) DEFAULT NULL,\n                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                updated_at DATETIME DEFAULT NULL,\n                PRIMARY KEY (id),\n                KEY idx_wlr_company_status (company_id, status),\n                KEY idx_wlr_request_location (company_id, request_location_id),\n                KEY idx_wlr_source_location (company_id, source_location_id),\n                KEY idx_wlr_date (request_date)\n            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci\n        ");
    } else {
        // Si la tabla ya existía por una versión anterior, completamos columnas sin perder datos.
        add_column_if_missing($pdo, 'warehouse_location_requests', 'company_id', "company_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER id");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'request_location_id', "request_location_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER company_id");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'source_location_id', "source_location_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER request_location_id");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'request_date', "request_date DATE NOT NULL DEFAULT (CURRENT_DATE) AFTER source_location_id");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'required_date', "required_date DATE DEFAULT NULL AFTER request_date");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'status', "status VARCHAR(30) NOT NULL DEFAULT 'PENDIENTE' AFTER required_date");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'requested_by', "requested_by BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER status");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'approved_by', "approved_by BIGINT(20) UNSIGNED DEFAULT NULL AFTER requested_by");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'approved_at', "approved_at DATETIME DEFAULT NULL AFTER approved_by");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'rejected_by', "rejected_by BIGINT(20) UNSIGNED DEFAULT NULL AFTER approved_at");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'rejected_at', "rejected_at DATETIME DEFAULT NULL AFTER rejected_by");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'reject_reason', "reject_reason VARCHAR(255) DEFAULT NULL AFTER rejected_at");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'shipped_by', "shipped_by BIGINT(20) UNSIGNED DEFAULT NULL AFTER reject_reason");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'shipped_at', "shipped_at DATETIME DEFAULT NULL AFTER shipped_by");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'received_by', "received_by BIGINT(20) UNSIGNED DEFAULT NULL AFTER shipped_at");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'received_at', "received_at DATETIME DEFAULT NULL AFTER received_by");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'driver_name', "driver_name VARCHAR(120) DEFAULT NULL AFTER received_at");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'vehicle_unit', "vehicle_unit VARCHAR(80) DEFAULT NULL AFTER driver_name");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'notes', "notes VARCHAR(255) DEFAULT NULL AFTER vehicle_unit");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'created_at', "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER notes");
        add_column_if_missing($pdo, 'warehouse_location_requests', 'updated_at', "updated_at DATETIME DEFAULT NULL AFTER created_at");
        add_index_if_missing($pdo, 'warehouse_location_requests', 'idx_wlr_company_status', "INDEX idx_wlr_company_status (company_id, status)");
        add_index_if_missing($pdo, 'warehouse_location_requests', 'idx_wlr_request_location', "INDEX idx_wlr_request_location (company_id, request_location_id)");
        add_index_if_missing($pdo, 'warehouse_location_requests', 'idx_wlr_source_location', "INDEX idx_wlr_source_location (company_id, source_location_id)");
        add_index_if_missing($pdo, 'warehouse_location_requests', 'idx_wlr_date', "INDEX idx_wlr_date (request_date)");
    }

    if (!table_exists($pdo, 'warehouse_location_request_items')) {
        $pdo->exec("\n            CREATE TABLE warehouse_location_request_items (\n                id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,\n                request_id BIGINT(20) UNSIGNED NOT NULL,\n                product_id BIGINT(20) UNSIGNED NOT NULL,\n                requested_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,\n                shipped_qty DECIMAL(14,3) DEFAULT NULL,\n                received_qty DECIMAL(14,3) DEFAULT NULL,\n                source_warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL,\n                destination_warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL,\n                inventory_move_out_id BIGINT(20) UNSIGNED DEFAULT NULL,\n                inventory_move_in_id BIGINT(20) UNSIGNED DEFAULT NULL,\n                notes VARCHAR(255) DEFAULT NULL,\n                PRIMARY KEY (id),\n                KEY idx_wlri_request (request_id),\n                KEY idx_wlri_product (product_id),\n                KEY idx_wlri_source_wh (source_warehouse_id),\n                KEY idx_wlri_dest_wh (destination_warehouse_id)\n            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci\n        ");
    } else {
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'request_id', "request_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER id");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'product_id', "product_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER request_id");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'requested_qty', "requested_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER product_id");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'shipped_qty', "shipped_qty DECIMAL(14,3) DEFAULT NULL AFTER requested_qty");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'received_qty', "received_qty DECIMAL(14,3) DEFAULT NULL AFTER shipped_qty");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'source_warehouse_id', "source_warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER received_qty");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'destination_warehouse_id', "destination_warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER source_warehouse_id");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'inventory_move_out_id', "inventory_move_out_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER destination_warehouse_id");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'inventory_move_in_id', "inventory_move_in_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER inventory_move_out_id");
        add_column_if_missing($pdo, 'warehouse_location_request_items', 'notes', "notes VARCHAR(255) DEFAULT NULL AFTER inventory_move_in_id");
        add_index_if_missing($pdo, 'warehouse_location_request_items', 'idx_wlri_request', "INDEX idx_wlri_request (request_id)");
        add_index_if_missing($pdo, 'warehouse_location_request_items', 'idx_wlri_product', "INDEX idx_wlri_product (product_id)");
        add_index_if_missing($pdo, 'warehouse_location_request_items', 'idx_wlri_source_wh', "INDEX idx_wlri_source_wh (source_warehouse_id)");
        add_index_if_missing($pdo, 'warehouse_location_request_items', 'idx_wlri_dest_wh', "INDEX idx_wlri_dest_wh (destination_warehouse_id)");
    }
}

function locreq_internal_warehouse_codes(): array {
    return ['MERMA','APROV-MERMA','CUB','SOB-CUB','SIN-WH'];
}

function locreq_is_internal_warehouse_code(string $code): bool {
    return in_array(strtoupper(trim($code)), locreq_internal_warehouse_codes(), true);
}

/**
 * BReport es MES multiempresa/multiplanta:
 * La existencia operativa no debe depender de códigos fijos como CF1/CF2/CF3.
 * Cada planta puede tener almacenes propios (ej. ALM01, CUA, etc.).
 * Estas funciones permiten usar reglas configuradas cuando existen y, si no,
 * tomar los almacenes activos de la localidad.
 */
function warehouse_is_disposal_code(string $code): bool {
    $code = strtoupper(trim($code));
    return in_array($code, ['MERMA','APROV-MERMA','SIN-WH'], true);
}

function get_active_operational_warehouses(PDO $pdo, int $companyId, int $locationId, bool $excludeDisposal = true): array {
    $st = $pdo->prepare("
        SELECT id, code, name
        FROM warehouses
        WHERE company_id = ?
          AND location_id = ?
          AND active = 1
        ORDER BY FIELD(UPPER(code),'CF1','CF2','CF3','ALM01','CUB','SOB-CUB','CUA','MERMA','APROV-MERMA'), code, name
    ");
    $st->execute([$companyId, $locationId]);

    $rows = [];
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $w) {
        $code = strtoupper(trim((string)$w['code']));
        if ($excludeDisposal && warehouse_is_disposal_code($code)) continue;
        if (botella_is_route_warehouse_code($code)) continue; // Las rutas son almacenes móviles; no son origen físico para surtir.
        $rows[] = [
            'warehouse_id' => (int)$w['id'],
            'code' => (string)$w['code'],
            'name' => (string)$w['name'],
        ];
    }
    return $rows;
}

function rule_warehouses_for_product_or_all(PDO $pdo, int $companyId, int $locationId, int $productId, string $purpose = 'source', bool $excludeDisposal = true): array {
    $purpose = strtolower(trim($purpose));
    $ids = [];

    if (table_exists($pdo, 'product_warehouse_rules')) {
        $st = $pdo->prepare("
            SELECT default_finished_warehouse_id, default_sales_warehouse_id, cubicado_warehouse_id
            FROM product_warehouse_rules
            WHERE company_id = ?
              AND location_id = ?
              AND product_id = ?
              AND active = 1
            LIMIT 1
        ");
        $st->execute([$companyId, $locationId, $productId]);
        $rule = $st->fetch(PDO::FETCH_ASSOC);
        if ($rule) {
            if ($purpose === 'source') {
                $ids[] = (int)($rule['default_sales_warehouse_id'] ?? 0);
                $ids[] = (int)($rule['default_finished_warehouse_id'] ?? 0);
                $ids[] = (int)($rule['cubicado_warehouse_id'] ?? 0);
            } else {
                $ids[] = (int)($rule['default_finished_warehouse_id'] ?? 0);
                $ids[] = (int)($rule['default_sales_warehouse_id'] ?? 0);
            }
            $rows = locreq_warehouse_rows_by_ids($pdo, $companyId, $locationId, $ids);
            if ($rows) return $rows;
        }
    }

    return get_active_operational_warehouses($pdo, $companyId, $locationId, $excludeDisposal);
}

function locreq_get_product_sku(PDO $pdo, int $companyId, int $productId): string {
    $product = get_product_row($pdo, $companyId, $productId);
    return strtoupper(trim((string)($product['sku'] ?? '')));
}

function locreq_warehouse_rows_by_ids(PDO $pdo, int $companyId, int $locationId, array $warehouseIds): array {
    $warehouseIds = array_values(array_unique(array_filter(array_map('intval', $warehouseIds))));
    if (!$warehouseIds) return [];

    $in = implode(',', array_fill(0, count($warehouseIds), '?'));
    $params = array_merge([$companyId, $locationId], $warehouseIds);
    $st = $pdo->prepare("\n        SELECT id, code, name\n        FROM warehouses\n        WHERE company_id = ?\n          AND location_id = ?\n          AND active = 1\n          AND id IN ($in)\n        ORDER BY FIELD(UPPER(code),'CF1','CF2','CF3'), code, name\n    ");
    $st->execute($params);

    $rows = [];
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $w) {
        $code = strtoupper(trim((string)$w['code']));
        /*
         * CUB/SOB-CUB son internos solamente para el flujo Hielo Barra.
         * En otras plantas se respetan los almacenes activos/configurados;
         * únicamente se excluyen almacenes de disposición.
         */
        if (breport_is_hielo_barra_scope() && locreq_is_internal_warehouse_code($code)) continue;
        if (!breport_is_hielo_barra_scope() && warehouse_is_disposal_code($code)) continue;
        $rows[] = [
            'warehouse_id' => (int)$w['id'],
            'code' => (string)$w['code'],
            'name' => (string)$w['name'],
        ];
    }
    return $rows;
}

function locreq_rule_warehouses_for_product(PDO $pdo, int $companyId, int $locationId, int $productId, string $purpose): array {
    /*
     * Solicitudes entre localidades:
     * - Hielo Barra conserva almacenes comerciales específicos.
     * - Otras plantas usan product_warehouse_rules o almacenes activos propios.
     */
    if (breport_is_hielo_barra_scope()) {
        $sku = locreq_get_product_sku($pdo, $companyId, $productId);
        $codes = $sku === 'BARRA 150 KILOS' ? ['CF1','CF2'] : ['CF3'];
        $rows = [];
        foreach ($codes as $code) {
            $wid = get_warehouse_id_by_code($pdo, $companyId, $locationId, $code);
            if ($wid > 0) {
                $rows[] = ['warehouse_id'=>$wid, 'code'=>$code, 'name'=>$code];
            }
        }
        return $rows;
    }

    return rule_warehouses_for_product_or_all($pdo, $companyId, $locationId, $productId, $purpose, true);
}

function locreq_commercial_wh_codes_for_product(PDO $pdo, int $companyId, int $productId): array {
    if (!breport_is_hielo_barra_scope()) return [];
    $sku = locreq_get_product_sku($pdo, $companyId, $productId);
    return $sku === 'BARRA 150 KILOS' ? ['CF1','CF2'] : ['CF3'];
}

function locreq_validate_commercial_warehouse(PDO $pdo, int $companyId, int $locationId, int $productId, int $warehouseId, string $label, string $purpose = 'source'): void {
    $code = get_warehouse_code_by_id($pdo, $companyId, $locationId, $warehouseId);
    if ($code === '') {
        throw new RuntimeException('Almacén ' . $label . ' inválido.');
    }
    if (breport_is_hielo_barra_scope() && locreq_is_internal_warehouse_code($code)) {
        throw new RuntimeException('Almacén ' . $label . ' no permitido para solicitud entre localidades de Hielo Barra. MERMA, APROV-MERMA, CUB y SOB-CUB son internos en este flujo.');
    }
    if (!breport_is_hielo_barra_scope() && warehouse_is_disposal_code($code)) {
        throw new RuntimeException('Almacén ' . $label . ' no permitido: los almacenes de disposición/merma no se utilizan para surtir solicitudes.');
    }

    $allowedRows = locreq_rule_warehouses_for_product($pdo, $companyId, $locationId, $productId, $purpose);
    $allowedIds = array_map(fn($w) => (int)$w['warehouse_id'], $allowedRows);
    if (!in_array($warehouseId, $allowedIds, true)) {
        $labels = array_map(fn($w) => (string)$w['code'], $allowedRows);
        $msg = $labels ? implode('/', $labels) : 'sin almacén configurado';
        throw new RuntimeException('Almacén ' . $label . ' no permitido para este producto/localidad. Permitidos: ' . $msg . '. Revisa Admin > Productos > configurar almacenes.');
    }
}

function locreq_get_location_name(PDO $pdo, int $locationId): string {
    $st = $pdo->prepare("SELECT name FROM locations WHERE id=? LIMIT 1");
    $st->execute([$locationId]);
    return (string)($st->fetchColumn() ?: ('Localidad #' . $locationId));
}

function locreq_stock_by_allowed_wh(PDO $pdo, int $companyId, int $locationId, int $productId, string $purpose = 'source'): array {
    $allowedRows = locreq_rule_warehouses_for_product($pdo, $companyId, $locationId, $productId, $purpose);
    $stock = build_product_warehouse_stock($pdo, $companyId, $locationId);
    $rows = [];
    foreach ($allowedRows as $w) {
        $wid = (int)$w['warehouse_id'];
        $rows[] = [
            'warehouse_id' => $wid,
            'code' => (string)$w['code'],
            'name' => (string)($w['name'] ?? ''),
            'qty' => (float)($stock[$productId][$wid] ?? 0.0),
        ];
    }
    return $rows;
}

function get_merma_concepts(PDO $pdo, int $companyId): array {
    $st = $pdo->prepare("\n        SELECT id, concept_type, name, sort_order\n        FROM quality_concepts\n        WHERE company_id = ?\n          AND concept_type = 'MERMA'\n          AND active = 1\n        ORDER BY sort_order, name\n    ");
    $st->execute([$companyId]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}

function ensure_default_merma_concepts(PDO $pdo, int $companyId): void {
    $st = $pdo->prepare("\n        SELECT COUNT(*)\n        FROM quality_concepts\n        WHERE company_id = ? AND concept_type = 'MERMA' AND active = 1\n    ");
    $st->execute([$companyId]);
    if ((int)$st->fetchColumn() > 0) return;

    $ins = $pdo->prepare("\n        INSERT INTO quality_concepts (company_id, concept_type, name, active, sort_order)\n        VALUES (?, 'MERMA', ?, 1, ?)\n    ");
    $ins->execute([$companyId, 'Merma en producción', 1]);
    $ins->execute([$companyId, 'Merma en Almacén', 2]);
}

function build_product_warehouse_stock(PDO $pdo, int $companyId, int $locationId): array {
    /**
     * FUENTE ÚNICA OFICIAL:
     * La existencia por producto/almacén se calcula solamente con inventory_moves.
     * - IN / ADJ suman.
     * - OUT resta.
     * - No se recortan negativos: si el histórico queda negativo, debe verse para corregirlo.
     *
     * Las tablas production_entries, cubing_entries, quality_entries, transfers, etc.
     * quedan como evidencia operativa y NO se mezclan aquí para evitar doble afectación.
     */
    $rows = [];

    $st = $pdo->prepare("\n        SELECT
            product_id,
            warehouse_id,
            SUM(CASE
                    WHEN move_type IN ('IN','ADJ') THEN qty
                    WHEN move_type = 'OUT' THEN -qty
                    ELSE 0
                END) AS qty
        FROM inventory_moves
        WHERE company_id = ?
          AND location_id = ?
          AND warehouse_id IS NOT NULL
        GROUP BY product_id, warehouse_id
    ");
    $st->execute([$companyId, $locationId]);

    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
        $pid = (int)$r['product_id'];
        $wid = (int)$r['warehouse_id'];
        if ($pid <= 0 || $wid <= 0) continue;
        $rows[$pid][$wid] = (float)$r['qty'];
    }

    return $rows;
}

function get_product_warehouse_available(PDO $pdo, int $companyId, int $locationId, int $productId, int $warehouseId): float {
    $map = build_product_warehouse_stock($pdo, $companyId, $locationId);
    return (float)($map[$productId][$warehouseId] ?? 0.0);
}

function is_bar_sku(string $sku): bool {
    return mb_strtoupper(trim($sku)) === 'BARRA 150 KILOS';
}

function get_bar_product_id(PDO $pdo, int $companyId): int {
    $st = $pdo->prepare("
        SELECT id
        FROM products
        WHERE company_id = ? AND sku = 'BARRA 150 KILOS'
        LIMIT 1
    ");
    $st->execute([$companyId]);
    return (int)($st->fetchColumn() ?: 0);
}

function is_commercial_bar_warehouse(string $code): bool {
    $code = mb_strtoupper(trim($code));
    return in_array($code, ['CF1', 'CF2'], true);
}

function get_active_warehouses_map(PDO $pdo, int $companyId, int $locationId): array {
    $st = $pdo->prepare("
        SELECT id, code, name
        FROM warehouses
        WHERE company_id = ?
          AND location_id = ?
          AND active = 1
        ORDER BY name
    ");
    $st->execute([$companyId, $locationId]);

    $map = [];
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $w) {
        $map[(int)$w['id']] = [
            'id'   => (int)$w['id'],
            'code' => (string)$w['code'],
            'name' => (string)$w['name'],
        ];
    }
    return $map;
}

function get_warehouse_id_by_code(PDO $pdo, int $companyId, int $locationId, string $code): int {
    $st = $pdo->prepare("\n        SELECT id
        FROM warehouses
        WHERE company_id = ?
          AND location_id = ?
          AND active = 1
          AND UPPER(code) = UPPER(?)
        LIMIT 1
    ");
    $st->execute([$companyId, $locationId, $code]);
    return (int)($st->fetchColumn() ?: 0);
}


function get_warehouse_code_by_id(PDO $pdo, int $companyId, int $locationId, int $warehouseId): string {
    if ($warehouseId <= 0) return '';
    $st = $pdo->prepare("\n        SELECT code\n        FROM warehouses\n        WHERE id = ? AND company_id = ? AND location_id = ? AND active = 1\n        LIMIT 1\n    ");
    $st->execute([$warehouseId, $companyId, $locationId]);
    return strtoupper(trim((string)($st->fetchColumn() ?: '')));
}

function ensure_merma_warehouse(PDO $pdo, int $companyId, int $locationId): int {
    $st = $pdo->prepare("\n        SELECT id\n        FROM warehouses\n        WHERE company_id = ?\n          AND location_id = ?\n          AND active = 1\n          AND UPPER(code) = 'MERMA'\n        LIMIT 1\n    ");
    $st->execute([$companyId, $locationId]);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id > 0) return $id;

    $st = $pdo->prepare("\n        INSERT INTO warehouses(company_id, location_id, code, name, active, created_at)\n        VALUES(?, ?, 'MERMA', 'Merma pendiente de disposición', 1, NOW())\n    ");
    $st->execute([$companyId, $locationId]);
    return (int)$pdo->lastInsertId();
}

function validate_commercial_source_for_merma(PDO $pdo, int $companyId, int $locationId, int $productId, int $warehouseId): void {
    $product = get_product_row($pdo, $companyId, $productId);
    if (!$product) {
        throw new RuntimeException('Producto inválido para registrar merma.');
    }

    $sku = strtoupper(trim((string)($product['sku'] ?? '')));
    $code = get_warehouse_code_by_id($pdo, $companyId, $locationId, $warehouseId);

    if ($code === '') {
        throw new RuntimeException('Almacén inválido para registrar merma.');
    }

    /*
     * Para Hielo Barra mantenemos protección operativa de la barra 150 kg.
     * Para otras plantas/productos, BReport no debe forzar CF3: se permite
     * descontar merma desde cualquier almacén activo operativo de esa planta,
     * excepto almacenes de disposición de merma.
     */
    if (breport_is_hielo_barra_scope() && $sku === 'BARRA 150 KILOS') {
        if (!in_array($code, ['CF1','CF2','CUB','SOB-CUB'], true)) {
            throw new RuntimeException('La merma de BARRA 150 KILOS solo puede salir de almacenes operativos de barra: CF1, CF2, CUB o SOB-CUB.');
        }
        return;
    }

    if (warehouse_is_disposal_code($code)) {
        throw new RuntimeException('No puedes registrar merma tomando producto desde almacenes de disposición como MERMA/APROV-MERMA.');
    }
}

function validate_internal_transfer_rule(PDO $pdo, int $companyId, int $locationId, int $productId, int $fromWarehouseId, int $toWarehouseId): void {
    $product = get_product_row($pdo, $companyId, $productId);
    if (!$product) {
        throw new RuntimeException('Producto inválido para transferencia.');
    }
    $sku = strtoupper(trim((string)($product['sku'] ?? '')));
    $fromCode = get_warehouse_code_by_id($pdo, $companyId, $locationId, $fromWarehouseId);
    $toCode   = get_warehouse_code_by_id($pdo, $companyId, $locationId, $toWarehouseId);

    if ($fromCode === '' || $toCode === '') {
        throw new RuntimeException('Almacén origen o destino inválido.');
    }

    if (warehouse_is_disposal_code($fromCode) || warehouse_is_disposal_code($toCode)) {
        throw new RuntimeException('MERMA/APROV-MERMA no se mueven por transferencia normal; usa el flujo de merma/calidad.');
    }

    /*
     * Hielo Barra conserva su regla especial.
     * El resto de plantas/productos pueden transferirse entre almacenes activos
     * de la localidad, porque BReport se usa como MES multiempresa/multiplanta.
     */
    if (breport_is_hielo_barra_scope() && $sku === 'BARRA 150 KILOS') {
        if (!in_array($fromCode, ['CF1','CF2','CUB','SOB-CUB'], true) || !in_array($toCode, ['CF1','CF2','CUB','SOB-CUB'], true)) {
            throw new RuntimeException('BARRA 150 KILOS solo puede transferirse entre almacenes operativos de barra: CF1, CF2, CUB y SOB-CUB.');
        }
    }
}

function get_product_row(PDO $pdo, int $companyId, int $productId): ?array {
    $st = $pdo->prepare("\n        SELECT id, sku, name, uom
        FROM products
        WHERE company_id = ?
          AND active = 1
          AND id = ?
        LIMIT 1
    ");
    $st->execute([$companyId, $productId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
}

function get_product_total_available(PDO $pdo, int $companyId, int $locationId, int $productId): float {
    $st = $pdo->prepare("
        SELECT COALESCE(SUM(CASE
                    WHEN im.move_type IN ('IN','ADJ') THEN im.qty
                    WHEN im.move_type = 'OUT' THEN -im.qty
                    ELSE 0
                END), 0) AS qty
        FROM inventory_moves im
        LEFT JOIN warehouses w ON w.id = im.warehouse_id
        WHERE im.company_id = ?
          AND im.location_id = ?
          AND im.product_id = ?
          AND (w.id IS NULL OR UPPER(COALESCE(w.code,'')) NOT REGEXP '^RUTA[-_ ]?[0-9]+$')
    ");
    $st->execute([$companyId, $locationId, $productId]);
    return (float)($st->fetchColumn() ?: 0.0);
}


/**
 * Botella · Disponibilidad inteligente ALM01 para bandeja de salidas.
 * ------------------------------------------------------------
 * Solo analiza el almacén principal ALM01. Las rutas RUTA-xx y otros
 * almacenes móviles no participan en esta predicción.
 */
function botella_alm01_stock_map(PDO $pdo, int $companyId, int $locationId): array {
    $alm01Id = get_warehouse_id_by_code($pdo, $companyId, $locationId, 'ALM01');
    if ($alm01Id <= 0) return [];

    $st = $pdo->prepare("
        SELECT
            im.product_id,
            COALESCE(SUM(CASE
                WHEN im.move_type IN ('IN','ADJ') THEN im.qty
                WHEN im.move_type = 'OUT' THEN -im.qty
                ELSE 0
            END),0) AS qty
        FROM inventory_moves im
        WHERE im.company_id = ?
          AND im.location_id = ?
          AND im.warehouse_id = ?
        GROUP BY im.product_id
    ");
    $st->execute([$companyId, $locationId, $alm01Id]);

    $map = [];
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
        $map[(int)$r['product_id']] = (float)$r['qty'];
    }
    return $map;
}

function botella_stock_worst_status(string $current, string $next): string {
    $rank = ['ok'=>0, 'stable'=>1, 'warning'=>2, 'danger'=>3, 'processed'=>-1];
    $cr = $rank[$current] ?? 0;
    $nr = $rank[$next] ?? 0;
    return $nr > $cr ? $next : $current;
}

/**
 * Enriquece las órdenes de la bandeja con semáforo acumulado:
 * - Verde: se alcanza y queda inventario estable.
 * - Amarillo: se alcanza, pero queda inventario bajo/casi igual.
 * - Rojo: ya no se alcanza considerando órdenes pendientes anteriores.
 */
function botella_enrich_sales_dispatch_stock(PDO $pdo, int $companyId, int $locationId, array $rows): array {
    if (!$rows) return $rows;

    $ids = array_values(array_unique(array_map(fn($r) => (int)($r['id'] ?? 0), $rows)));
    $ids = array_values(array_filter($ids));
    if (!$ids) return $rows;

    $in = implode(',', array_fill(0, count($ids), '?'));
    $st = $pdo->prepare("
        SELECT
            i.order_id,
            i.product_id,
            p.sku,
            p.name AS product_name,
            COALESCE(SUM(i.qty),0) AS qty
        FROM sales_order_items i
        JOIN products p ON p.id = i.product_id
        WHERE i.order_id IN ($in)
        GROUP BY i.order_id, i.product_id, p.sku, p.name
        ORDER BY i.order_id, p.sku
    ");
    $st->execute($ids);

    $itemsByOrder = [];
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $it) {
        $oid = (int)$it['order_id'];
        if (!isset($itemsByOrder[$oid])) $itemsByOrder[$oid] = [];
        $itemsByOrder[$oid][] = [
            'product_id' => (int)$it['product_id'],
            'sku' => (string)$it['sku'],
            'product_name' => (string)$it['product_name'],
            'qty' => (float)$it['qty'],
        ];
    }

    $rolling = botella_alm01_stock_map($pdo, $companyId, $locationId);

    foreach ($rows as &$r) {
        $oid = (int)($r['id'] ?? 0);
        $items = $itemsByOrder[$oid] ?? [];
        $status = 'stable';
        $messages = [];
        $stockItems = [];

        $isPending = strtoupper((string)($r['status'] ?? '')) === 'AUTORIZADA' && empty($r['outbound_id']);

        if (!$isPending) {
            $r['stock_status'] = 'processed';
            $r['stock_label'] = 'ORDEN PROCESADA';
            $r['stock_message'] = 'La orden ya tiene avance/salida y no se considera para la predicción pendiente.';
            $r['stock_items'] = [];
            continue;
        }

        foreach ($items as $it) {
            $pid = (int)$it['product_id'];
            $requested = round((float)$it['qty'], 3);
            $before = round((float)($rolling[$pid] ?? 0.0), 3);
            $after = round($before - $requested, 3);

            $itemStatus = 'stable';
            $itemMessage = 'Inventario estable para surtir.';
            if ($requested > $before + 0.0001) {
                $itemStatus = 'danger';
                $itemMessage = 'No alcanza con ALM01 considerando órdenes pendientes anteriores.';
                $messages[] = 'requiere revisión de existencias';
            } else {
                $ratio = $before > 0 ? ($requested / $before) : 1;
                if ($ratio >= 0.80 || $after <= max(10.0, $before * 0.20)) {
                    $itemStatus = 'warning';
                    $itemMessage = 'Se alcanza, pero queda inventario bajo en ALM01.';
                    $messages[] = 'inventario justo';
                }
            }

            $status = botella_stock_worst_status($status, $itemStatus);
            $stockItems[] = [
                'product_id' => $pid,
                'sku' => $it['sku'],
                'product_name' => $it['product_name'],
                'requested_qty' => $requested,
                'available_before' => $before,
                'available_after' => $after,
                'stock_status' => $itemStatus,
                'stock_message' => $itemMessage,
            ];

            /*
             * La predicción consume la disponibilidad aunque no alcance, para que
             * las órdenes posteriores reflejen correctamente el faltante acumulado.
             */
            $rolling[$pid] = max(0.0, $after);
        }

        if ($status === 'danger') {
            $label = 'NO ALCANZA';
            $msg = 'Esta orden ya no se alcanzaría a surtir con ALM01 considerando las órdenes pendientes anteriores.';
        } elseif ($status === 'warning') {
            $label = 'INVENTARIO JUSTO';
            $msg = 'Se puede surtir, pero la cantidad solicitada es casi similar al disponible en ALM01.';
        } else {
            $label = 'INVENTARIO ESTABLE';
            $msg = 'Inventario suficiente en ALM01 para surtir esta orden.';
        }

        $r['stock_status'] = $status;
        $r['stock_label'] = $label;
        $r['stock_message'] = $msg;
        $r['stock_items'] = $stockItems;
    }
    unset($r);

    return $rows;
}


/**
 * Almacenes origen permitidos para surtir Botella.
 * ------------------------------------------------------------
 * Regla crítica:
 * - Se toma primero product_warehouse_rules cuando existe.
 * - Para salida se prioriza default_out_warehouse_id, luego default_sales_warehouse_id,
 *   default_finished_warehouse_id y cubicado_warehouse_id como compatibilidad.
 * - Nunca se permite surtir desde almacenes de ruta RUTA-xx, porque son almacenes
 *   móviles de reparto y no inventario disponible general.
 * - Nunca se permite surtir desde MERMA/APROV-MERMA/SIN-WH.
 * - Si no hay regla configurada, usa almacenes activos operativos de la localidad.
 */
function outbound_source_warehouses(PDO $pdo, int $companyId, int $locationId, int $productId): array {
    $rows = [];
    $seen = [];

    $addRow = function(array $w) use (&$rows, &$seen): void {
        $wid = (int)($w['warehouse_id'] ?? $w['id'] ?? 0);
        $code = strtoupper(trim((string)($w['code'] ?? '')));
        if ($wid <= 0 || $code === '') return;
        if (isset($seen[$wid])) return;
        if (warehouse_is_disposal_code($code)) return;
        if (botella_is_route_warehouse_code($code)) return;

        $seen[$wid] = true;
        $rows[] = [
            'warehouse_id' => $wid,
            'code' => (string)($w['code'] ?? ''),
            'name' => (string)($w['name'] ?? $w['code'] ?? ''),
        ];
    };

    if (table_exists($pdo, 'product_warehouse_rules')) {
        try {
            $st = $pdo->prepare("\n                SELECT\n                    default_out_warehouse_id,\n                    default_sales_warehouse_id,\n                    default_finished_warehouse_id,\n                    cubicado_warehouse_id\n                FROM product_warehouse_rules\n                WHERE company_id = ?\n                  AND location_id = ?\n                  AND product_id = ?\n                  AND active = 1\n                LIMIT 1\n            ");
            $st->execute([$companyId, $locationId, $productId]);
            $rule = $st->fetch(PDO::FETCH_ASSOC);
            if ($rule) {
                $ids = [
                    (int)($rule['default_out_warehouse_id'] ?? 0),
                    (int)($rule['default_sales_warehouse_id'] ?? 0),
                    (int)($rule['default_finished_warehouse_id'] ?? 0),
                    (int)($rule['cubicado_warehouse_id'] ?? 0),
                ];
                foreach (locreq_warehouse_rows_by_ids($pdo, $companyId, $locationId, $ids) as $w) {
                    $addRow($w);
                }
            }
        } catch (Throwable $e) {
            // Si el servidor tiene una versión antigua de la tabla/regla,
            // se usa fallback seguro por almacenes activos operativos.
        }
    }

    if (!$rows) {
        foreach (get_active_operational_warehouses($pdo, $companyId, $locationId, true) as $w) {
            $addRow($w);
        }
    }

    return $rows;
}

function get_product_outbound_available(PDO $pdo, int $companyId, int $locationId, int $productId): float {
    $stockByWh = build_product_warehouse_stock($pdo, $companyId, $locationId);
    $total = 0.0;
    foreach (outbound_source_warehouses($pdo, $companyId, $locationId, $productId) as $w) {
        $total += (float)($stockByWh[$productId][(int)$w['warehouse_id']] ?? 0.0);
    }
    return round($total, 3);
}

function allocate_inventory_out(PDO $pdo, int $companyId, int $locationId, int $productId, float $qty): array {
    /**
     * Asignación real de salida conforme a la planta activa.
     * El inventario se obtiene exclusivamente de inventory_moves.
     */
    $qty = round($qty, 3);
    if ($qty <= 0) return [];

    $product = get_product_row($pdo, $companyId, $productId);
    $sku = strtoupper(trim((string)($product['sku'] ?? '')));
    $stockByWh = build_product_warehouse_stock($pdo, $companyId, $locationId);
    $candidateRows = outbound_source_warehouses($pdo, $companyId, $locationId, $productId);

    if (!$candidateRows) {
        throw new RuntimeException('No existe almacén de salida configurado para el producto ' . ($sku !== '' ? $sku : ('#' . $productId)) . ' en esta planta.');
    }

    $alloc = [];
    $remaining = $qty;

    foreach ($candidateRows as $w) {
        $wid = (int)($w['warehouse_id'] ?? 0);
        if ($wid <= 0) continue;
        $available = (float)($stockByWh[$productId][$wid] ?? 0.0);
        if ($available <= 0.0001) continue;

        $take = min($remaining, $available);
        if ($take > 0.0001) {
            $alloc[] = ['warehouse_id' => $wid, 'qty' => round($take, 3)];
            $remaining = round($remaining - $take, 3);
        }
        if ($remaining <= 0.0001) break;
    }

    if ($remaining > 0.0001) {
        $labels = array_map(fn($w) => (string)($w['code'] ?? ''), $candidateRows);
        throw new RuntimeException(
            'Inventario insuficiente en almacenes de salida permitidos (' .
            implode('/', $labels) . '). Faltante: ' . number_format($remaining, 3, '.', '')
        );
    }

    return $alloc;
}



/* ============================================================
   ENTRADA MANUAL TEMPORAL DE EXISTENCIAS · PLANTAS SIN PRODUCCIÓN
============================================================ */

/**
 * Devuelve almacenes permitidos de la planta activa para cargar existencia.
 * Respeta reglas por producto/localidad cuando existen; en caso contrario
 * utiliza almacenes activos operativos de esa misma localidad.
 * Nunca permite almacenes de disposición MERMA/APROV-MERMA/SIN-WH.
 */
function manual_stock_in_allowed_warehouses(PDO $pdo, int $companyId, int $locationId, int $productId): array {
    if (!breport_is_manual_existence_scope()) {
        throw new RuntimeException('Agregar existencias está disponible únicamente para Botella, Hielo Bolsa y Garrafón.');
    }

    $rows = rule_warehouses_for_product_or_all($pdo, $companyId, $locationId, $productId, 'dest', true);
    $valid = [];
    foreach ($rows as $w) {
        $wid = (int)($w['warehouse_id'] ?? 0);
        $code = strtoupper(trim((string)($w['code'] ?? '')));
        if ($wid <= 0 || $code === '' || warehouse_is_disposal_code($code)) continue;
        $valid[] = [
            'warehouse_id' => $wid,
            'code' => (string)($w['code'] ?? ''),
            'name' => (string)($w['name'] ?? ''),
            'qty' => get_product_warehouse_available($pdo, $companyId, $locationId, $productId, $wid),
        ];
    }
    return $valid;
}

/**
 * Embalaje para recargas/entradas de Botella.
 * Primero intenta product_packaging_options y si aún no está migrado usa
 * products.pieces_per_package / products.packages_per_pallet.
 */
function manual_stock_in_packaging_row(PDO $pdo, int $companyId, int $productId, int $packagingId): array {
    if ($packagingId > 0) {
        try {
            $st = $pdo->prepare("
                SELECT id, product_id, packaging_type, packaging_name,
                       pieces_per_package, packages_per_pallet, is_default, active,
                       0 AS legacy_fallback
                FROM product_packaging_options
                WHERE id = ?
                  AND company_id = ?
                  AND product_id = ?
                  AND active = 1
                LIMIT 1
            ");
            $st->execute([$packagingId, $companyId, $productId]);
            $row = $st->fetch(PDO::FETCH_ASSOC);
            if ($row) return $row;
        } catch (Throwable $e) {
            // Si la tabla aún no está disponible, se usa fallback de products.
        }
    }

    $st = $pdo->prepare("
        SELECT
            0 AS id,
            id AS product_id,
            'TARIMA' AS packaging_type,
            CONCAT('Embalaje principal · ', packages_per_pallet, ' paq/rack') AS packaging_name,
            pieces_per_package,
            packages_per_pallet,
            1 AS is_default,
            active,
            1 AS legacy_fallback
        FROM products
        WHERE id = ?
          AND company_id = ?
          AND active = 1
          AND COALESCE(pieces_per_package,0) > 0
          AND COALESCE(packages_per_pallet,0) > 0
        LIMIT 1
    ");
    $st->execute([$productId, $companyId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) {
        throw new RuntimeException('El artículo no tiene embalaje configurado en Admin o en el producto.');
    }
    return $row;
}

function manual_stock_in_packaging_options(PDO $pdo, int $companyId, int $productId): array {
    $rows = [];
    try {
        $st = $pdo->prepare("
            SELECT
                po.id,
                po.product_id,
                po.packaging_type,
                po.packaging_name,
                po.pieces_per_package,
                po.packages_per_pallet,
                po.is_default,
                0 AS legacy_fallback,
                (po.pieces_per_package * po.packages_per_pallet) AS pieces_per_pallet
            FROM product_packaging_options po
            JOIN products p ON p.id = po.product_id AND p.company_id = po.company_id
            WHERE po.company_id = ?
              AND po.product_id = ?
              AND po.active = 1
              AND p.active = 1
            ORDER BY po.is_default DESC, po.packaging_type, po.packages_per_pallet DESC
        ");
        $st->execute([$companyId, $productId]);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);
    } catch (Throwable $e) {
        $rows = [];
    }

    if (!$rows) {
        $row = manual_stock_in_packaging_row($pdo, $companyId, $productId, 0);
        $rows[] = [
            'id' => 0,
            'product_id' => (int)$row['product_id'],
            'packaging_type' => (string)$row['packaging_type'],
            'packaging_name' => (string)$row['packaging_name'],
            'pieces_per_package' => (int)$row['pieces_per_package'],
            'packages_per_pallet' => (int)$row['packages_per_pallet'],
            'is_default' => 1,
            'legacy_fallback' => 1,
            'pieces_per_pallet' => ((int)$row['pieces_per_package'] * (int)$row['packages_per_pallet']),
        ];
    }

    return $rows;
}

/**
 * Valida la captura antes de vista previa y antes de guardar.
 * Las existencias comerciales se capturan como piezas enteras; no se redondea
 * una captura decimal para evitar impactos incorrectos en inventario.
 */
function validate_manual_stock_in_payload(PDO $pdo, int $companyId, int $locationId, array $d): array {
    if (!breport_is_manual_existence_scope()) {
        throw new RuntimeException('Agregar existencias no está habilitado para esta planta.');
    }

    $productId = (int)arr_get($d, 'product_id', 0);
    $warehouseId = (int)arr_get($d, 'warehouse_id', 0);
    $packagingId = (int)arr_get($d, 'packaging_option_id', 0);
    $rackQtyRaw = (float)arr_get($d, 'rack_qty', 0);
    $loosePackagesRaw = (float)arr_get($d, 'loose_packages', 0);
    $loosePiecesRaw = (float)arr_get($d, 'loose_pieces', 0);
    $lineName = trim((string)arr_get($d, 'line_name', ''));
    $provider = trim((string)arr_get($d, 'provider', ''));
    $routeNo = (int)arr_get($d, 'route_no', 0);
    $routeLabel = trim((string)arr_get($d, 'route_label', ''));
    $notes = trim((string)arr_get($d, 'notes', ''));

    if ($productId <= 0 || $warehouseId <= 0) {
        throw new RuntimeException('Artículo y almacén son obligatorios.');
    }
    foreach ([
        'rack_qty' => $rackQtyRaw,
        'loose_packages' => $loosePackagesRaw,
        'loose_pieces' => $loosePiecesRaw,
    ] as $field => $value) {
        if ($value < 0) {
            throw new RuntimeException('No se permiten cantidades negativas.');
        }
        if (abs($value - round($value)) > 0.000001) {
            throw new RuntimeException('Las recargas deben capturarse en números enteros. Revisa racks/tarimas, paquetes sueltos o piezas sueltas.');
        }
    }

    $rackQty = (int)round($rackQtyRaw);
    $loosePackages = (int)round($loosePackagesRaw);
    $loosePieces = (int)round($loosePiecesRaw);

    if ($rackQty <= 0 && $loosePackages <= 0 && $loosePieces <= 0) {
        throw new RuntimeException('Captura al menos racks/tarimas, paquetes sueltos o piezas sueltas.');
    }

    $product = get_product_row($pdo, $companyId, $productId);
    if (!$product) {
        throw new RuntimeException('El artículo seleccionado no pertenece a la planta activa o no está activo.');
    }

    $packaging = manual_stock_in_packaging_row($pdo, $companyId, $productId, $packagingId);
    $packagesPerRack = (int)($packaging['packages_per_pallet'] ?? 0);
    $piecesPerPackage = (int)($packaging['pieces_per_package'] ?? 0);
    if ($packagesPerRack <= 0 || $piecesPerPackage <= 0) {
        throw new RuntimeException('El embalaje del artículo no tiene paquetes por rack/tarima o piezas por paquete válidas.');
    }

    $totalPackages = ($rackQty * $packagesPerRack) + $loosePackages;
    $totalPieces = ($totalPackages * $piecesPerPackage) + $loosePieces;

    if ($totalPieces <= 0) {
        throw new RuntimeException('El cálculo de piezas a ingresar debe ser mayor a cero.');
    }

    $warehouse = null;
    foreach (manual_stock_in_allowed_warehouses($pdo, $companyId, $locationId, $productId) as $w) {
        if ((int)$w['warehouse_id'] === $warehouseId) {
            $warehouse = $w;
            break;
        }
    }
    if (!$warehouse) {
        throw new RuntimeException('El almacén seleccionado no está permitido para este artículo y localidad.');
    }

    if ($routeNo > 0) {
        try {
            $stRoute = $pdo->prepare("
                SELECT route_code, route_name
                FROM route_access_codes
                WHERE company_id = ?
                  AND location_id = ?
                  AND route_no = ?
                  AND active = 1
                LIMIT 1
            ");
            $stRoute->execute([$companyId, $locationId, $routeNo]);
            $rr = $stRoute->fetch(PDO::FETCH_ASSOC);
            if ($rr) {
                $routeLabel = trim((string)($rr['route_code'] ?? '') . ' · ' . (string)($rr['route_name'] ?? ''));
            }
        } catch (Throwable $e) {
            // La ruta queda como dato informativo en notas si la tabla no existe.
        }
    }

    return [
        'work_date' => breport_operational_today(),
        'product_id' => $productId,
        'product_sku' => (string)($product['sku'] ?? ''),
        'product_name' => (string)($product['name'] ?? ''),
        'uom' => (string)($product['uom'] ?? 'pz'),
        'warehouse_id' => $warehouseId,
        'warehouse_code' => (string)($warehouse['code'] ?? ''),
        'warehouse_name' => (string)($warehouse['name'] ?? ''),
        'packaging_option_id' => $packagingId > 0 ? $packagingId : null,
        'packaging_name' => (string)($packaging['packaging_name'] ?? $packaging['packaging_type'] ?? ''),
        'rack_qty' => $rackQty,
        'packages_per_rack' => $packagesPerRack,
        'loose_packages' => $loosePackages,
        'total_packages' => $totalPackages,
        'pieces_per_package' => $piecesPerPackage,
        'loose_pieces' => $loosePieces,
        'qty' => (float)$totalPieces,
        'line_name' => mb_substr($lineName, 0, 60, 'UTF-8'),
        'provider' => mb_substr($provider, 0, 80, 'UTF-8'),
        'route_no' => $routeNo > 0 ? $routeNo : null,
        'route_label' => mb_substr($routeLabel, 0, 160, 'UTF-8'),
        'notes' => mb_substr($notes, 0, 160, 'UTF-8'),
    ];
}



/* ============================================================
   BOTELLA · PLAN DE CARGA Y ORDEN A MONTACARGAS
============================================================ */

/**
 * Botella · Las rutas se administran como almacenes móviles.
 * ------------------------------------------------------------
 * Regla:
 * - Almacén carga una ruta, no descuenta definitivamente inventario de la empresa.
 * - La carga genera OUT del almacén físico origen e IN al almacén móvil de la ruta.
 * - El saldo que queda en la ruta permanece como inventario inicial del día siguiente.
 */
function botella_route_warehouse_code(int $routeNo): string {
    return 'RUTA-' . str_pad((string)$routeNo, 2, '0', STR_PAD_LEFT);
}

function botella_is_route_warehouse_code(string $code): bool {
    return (bool)preg_match('/^RUTA[-_ ]?\d+$/i', trim($code));
}

function botella_route_row(PDO $pdo, int $companyId, int $locationId, int $routeNo): array {
    if ($routeNo <= 0) {
        throw new RuntimeException('La orden no tiene ruta asignada. Regresa a Ventas Botella y selecciona la ruta antes de cargar.');
    }
    $st = $pdo->prepare("
        SELECT id, route_no, route_code, route_name, driver_name, vehicle_unit
        FROM route_access_codes
        WHERE company_id = ?
          AND location_id = ?
          AND route_no = ?
          AND active = 1
        LIMIT 1
    ");
    $st->execute([$companyId, $locationId, $routeNo]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) {
        throw new RuntimeException('La ruta #' . $routeNo . ' no está activa o no existe en Admin para esta planta.');
    }
    return $row;
}

function botella_ensure_route_warehouse(PDO $pdo, int $companyId, int $locationId, int $routeNo): int {
    $route = botella_route_row($pdo, $companyId, $locationId, $routeNo);
    $code = botella_route_warehouse_code($routeNo);
    $nameParts = ['Ruta ' . $routeNo];
    if (!empty($route['route_code'])) $nameParts[] = (string)$route['route_code'];
    if (!empty($route['route_name'])) $nameParts[] = (string)$route['route_name'];
    $name = mb_substr(implode(' · ', $nameParts), 0, 120, 'UTF-8');

    $st = $pdo->prepare("
        SELECT id
        FROM warehouses
        WHERE company_id = ?
          AND location_id = ?
          AND UPPER(code) = UPPER(?)
        LIMIT 1
    ");
    $st->execute([$companyId, $locationId, $code]);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id > 0) {
        $st = $pdo->prepare("
            UPDATE warehouses
            SET name = ?, active = 1
            WHERE id = ? AND company_id = ? AND location_id = ?
        ");
        $st->execute([$name, $id, $companyId, $locationId]);
        return $id;
    }

    $st = $pdo->prepare("
        INSERT INTO warehouses(company_id, location_id, code, name, active, created_at)
        VALUES(?, ?, ?, ?, 1, NOW())
    ");
    $st->execute([$companyId, $locationId, $code, $name]);
    return (int)$pdo->lastInsertId();
}

function botella_route_stock(PDO $pdo, int $companyId, int $locationId, int $routeNo): array {
    $routeWarehouseId = botella_ensure_route_warehouse($pdo, $companyId, $locationId, $routeNo);
    $st = $pdo->prepare("
        SELECT
            p.id AS product_id,
            p.sku,
            p.name,
            p.uom,
            COALESCE(SUM(CASE
                WHEN im.move_type IN ('IN','ADJ') THEN im.qty
                WHEN im.move_type = 'OUT' THEN -im.qty
                ELSE 0
            END), 0) AS qty
        FROM products p
        LEFT JOIN inventory_moves im
          ON im.product_id = p.id
         AND im.company_id = p.company_id
         AND im.location_id = ?
         AND im.warehouse_id = ?
        WHERE p.company_id = ?
          AND p.active = 1
        GROUP BY p.id, p.sku, p.name, p.uom
        HAVING ABS(qty) > 0.0001
        ORDER BY p.sku, p.name
    ");
    $st->execute([$locationId, $routeWarehouseId, $companyId]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}


/**
 * Cliente interno usado únicamente para crear órdenes logísticas por ruta desde Almacén.
 * No representa un cliente comercial; permite reutilizar sales_orders/carga_camion sin inventar tablas.
 */
function botella_route_internal_customer_id(PDO $pdo, int $companyId, int $locationId, int $routeNo, array $route): int {
    $code = 'RUTA-' . str_pad((string)$routeNo, 2, '0', STR_PAD_LEFT);
    $business = 'Ruta ' . $routeNo;
    if (!empty($route['route_code'])) $business .= ' · ' . (string)$route['route_code'];
    if (!empty($route['route_name'])) $business .= ' · ' . (string)$route['route_name'];
    $business = mb_substr($business, 0, 150, 'UTF-8');

    $hasLocation = column_exists($pdo, 'sales_customers', 'location_id');
    $sql = "SELECT id FROM sales_customers WHERE company_id=? AND customer_code=? AND active=1";
    $params = [$companyId, $code];
    if ($hasLocation) { $sql .= " AND (location_id IS NULL OR location_id=?)"; $params[] = $locationId; }
    $sql .= " LIMIT 1";
    $st = $pdo->prepare($sql);
    $st->execute($params);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id > 0) {
        $upd = $pdo->prepare("UPDATE sales_customers SET business_name=?, contact_name=?, address=?, active=1, updated_at=NOW() WHERE id=? AND company_id=?");
        $upd->execute([$business, 'Orden logística por ruta', $business, $id, $companyId]);
        return $id;
    }

    if ($hasLocation) {
        $ins = $pdo->prepare("INSERT INTO sales_customers(company_id, location_id, customer_code, business_name, contact_name, address, active, created_at, updated_at) VALUES(?,?,?,?,?,?,1,NOW(),NOW())");
        $ins->execute([$companyId, $locationId, $code, $business, 'Orden logística por ruta', $business]);
    } else {
        $ins = $pdo->prepare("INSERT INTO sales_customers(company_id, customer_code, business_name, contact_name, address, active, created_at, updated_at) VALUES(?,?,?,?,?,1,NOW(),NOW())");
        $ins->execute([$companyId, $code, $business, 'Orden logística por ruta', $business]);
    }
    return (int)$pdo->lastInsertId();
}


function ensure_botella_forklift_schema(PDO $pdo): void {
    if (!table_exists($pdo, 'sales_truck_load_plans')) {
        $pdo->exec("CREATE TABLE sales_truck_load_plans (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            location_id BIGINT UNSIGNED NOT NULL,
            order_id BIGINT UNSIGNED NOT NULL,
            vehicle_type VARCHAR(30) NOT NULL DEFAULT 'CAJA_SECA',
            vehicle_label VARCHAR(80) NOT NULL DEFAULT 'Caja seca',
            capacity_pallets INT NOT NULL DEFAULT 30,
            status VARCHAR(30) NOT NULL DEFAULT 'SAVED',
            notes VARCHAR(255) DEFAULT NULL,
            created_by BIGINT UNSIGNED DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_by BIGINT UNSIGNED DEFAULT NULL,
            updated_at DATETIME DEFAULT NULL,
            PRIMARY KEY(id),
            UNIQUE KEY uq_stlp_order(company_id, location_id, order_id),
            KEY idx_stlp_scope(company_id, location_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }
    if (!table_exists($pdo, 'sales_truck_load_plan_slots')) {
        $pdo->exec("CREATE TABLE sales_truck_load_plan_slots (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            plan_id BIGINT UNSIGNED NOT NULL,
            slot_no INT NOT NULL,
            order_item_id BIGINT UNSIGNED DEFAULT NULL,
            product_id BIGINT UNSIGNED NOT NULL,
            customer_id BIGINT UNSIGNED DEFAULT NULL,
            address_id BIGINT UNSIGNED DEFAULT NULL,
            customer_label VARCHAR(180) DEFAULT NULL,
            delivery_address VARCHAR(255) DEFAULT NULL,
            purchase_order VARCHAR(80) DEFAULT NULL,
            product_label VARCHAR(180) DEFAULT NULL,
            pallets_qty DECIMAL(14,3) NOT NULL DEFAULT 1.000,
            packages_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            pieces_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            notes VARCHAR(255) DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY(id),
            KEY idx_stlps_plan(plan_id),
            KEY idx_stlps_product(product_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    } else {
        add_column_if_missing($pdo, 'sales_truck_load_plan_slots', 'customer_id', "customer_id BIGINT UNSIGNED DEFAULT NULL AFTER product_id");
        add_column_if_missing($pdo, 'sales_truck_load_plan_slots', 'address_id', "address_id BIGINT UNSIGNED DEFAULT NULL AFTER customer_id");
        add_column_if_missing($pdo, 'sales_truck_load_plan_slots', 'customer_label', "customer_label VARCHAR(180) DEFAULT NULL AFTER address_id");
        add_column_if_missing($pdo, 'sales_truck_load_plan_slots', 'delivery_address', "delivery_address VARCHAR(255) DEFAULT NULL AFTER customer_label");
    }

    if (!table_exists($pdo, 'warehouse_forklift_load_orders')) {
        $pdo->exec("CREATE TABLE warehouse_forklift_load_orders (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            location_id BIGINT UNSIGNED NOT NULL,
            order_id BIGINT UNSIGNED NOT NULL,
            truck_plan_id BIGINT UNSIGNED NOT NULL,
            status VARCHAR(30) NOT NULL DEFAULT 'PENDIENTE',
            vehicle_unit VARCHAR(80) NOT NULL,
            driver_name VARCHAR(120) NOT NULL,
            notes VARCHAR(255) DEFAULT NULL,
            created_by BIGINT UNSIGNED NOT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            confirmed_by BIGINT UNSIGNED DEFAULT NULL,
            confirmed_at DATETIME DEFAULT NULL,
            completed_notes VARCHAR(255) DEFAULT NULL,
            PRIMARY KEY(id),
            KEY idx_wflo_scope_status(company_id, location_id, status),
            KEY idx_wflo_order(company_id, location_id, order_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    } else {
        add_column_if_missing($pdo, 'warehouse_forklift_load_orders', 'truck_plan_id', "truck_plan_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER order_id");
        add_column_if_missing($pdo, 'warehouse_forklift_load_orders', 'vehicle_unit', "vehicle_unit VARCHAR(80) NOT NULL DEFAULT '' AFTER status");
        add_column_if_missing($pdo, 'warehouse_forklift_load_orders', 'driver_name', "driver_name VARCHAR(120) NOT NULL DEFAULT '' AFTER vehicle_unit");
        add_column_if_missing($pdo, 'warehouse_forklift_load_orders', 'confirmed_by', "confirmed_by BIGINT UNSIGNED DEFAULT NULL AFTER created_at");
        add_column_if_missing($pdo, 'warehouse_forklift_load_orders', 'confirmed_at', "confirmed_at DATETIME DEFAULT NULL AFTER confirmed_by");
        add_column_if_missing($pdo, 'warehouse_forklift_load_orders', 'completed_notes', "completed_notes VARCHAR(255) DEFAULT NULL AFTER confirmed_at");
    }

    if (!table_exists($pdo, 'warehouse_forklift_load_order_items')) {
        $pdo->exec("CREATE TABLE warehouse_forklift_load_order_items (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            forklift_order_id BIGINT UNSIGNED NOT NULL,
            slot_no INT NOT NULL,
            product_id BIGINT UNSIGNED NOT NULL,
            purchase_order VARCHAR(80) DEFAULT NULL,
            customer_label VARCHAR(180) DEFAULT NULL,
            delivery_address VARCHAR(255) DEFAULT NULL,
            product_label VARCHAR(180) DEFAULT NULL,
            pallets_qty DECIMAL(14,3) NOT NULL DEFAULT 1.000,
            packages_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            pieces_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            loaded TINYINT(1) NOT NULL DEFAULT 0,
            loaded_at DATETIME DEFAULT NULL,
            PRIMARY KEY(id),
            KEY idx_wfloi_order(forklift_order_id),
            KEY idx_wfloi_slot(forklift_order_id, slot_no)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'slot_no', "slot_no INT NOT NULL DEFAULT 0 AFTER forklift_order_id");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'product_id', "product_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER slot_no");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'purchase_order', "purchase_order VARCHAR(80) DEFAULT NULL AFTER product_id");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'customer_label', "customer_label VARCHAR(180) DEFAULT NULL AFTER purchase_order");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'delivery_address', "delivery_address VARCHAR(255) DEFAULT NULL AFTER customer_label");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'product_label', "product_label VARCHAR(180) DEFAULT NULL AFTER delivery_address");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'pallets_qty', "pallets_qty DECIMAL(14,3) NOT NULL DEFAULT 1.000 AFTER product_label");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'packages_qty', "packages_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER pallets_qty");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'pieces_qty', "pieces_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER packages_qty");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'loaded', "loaded TINYINT(1) NOT NULL DEFAULT 0 AFTER pieces_qty");
    add_column_if_missing($pdo, 'warehouse_forklift_load_order_items', 'loaded_at', "loaded_at DATETIME DEFAULT NULL AFTER loaded");
}

function botella_truck_plan_for_order(PDO $pdo, int $companyId, int $locationId, int $orderId): ?array {
    $st = $pdo->prepare("SELECT * FROM sales_truck_load_plans WHERE company_id=? AND location_id=? AND order_id=? LIMIT 1");
    $st->execute([$companyId, $locationId, $orderId]);
    $plan = $st->fetch(PDO::FETCH_ASSOC);
    return $plan ?: null;
}

function botella_truck_plan_slots(PDO $pdo, int $planId): array {
    $st = $pdo->prepare("SELECT * FROM sales_truck_load_plan_slots WHERE plan_id=? ORDER BY slot_no DESC");
    $st->execute([$planId]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}


function botella_order_items_as_truck_slots(PDO $pdo, int $companyId, int $locationId, int $orderId): array {
    $stOrder = $pdo->prepare("SELECT o.*, c.business_name, c.contact_name FROM sales_orders o JOIN sales_customers c ON c.id=o.customer_id WHERE o.id=? AND o.company_id=? AND o.location_id=? LIMIT 1");
    $stOrder->execute([$orderId, $companyId, $locationId]);
    $order = $stOrder->fetch(PDO::FETCH_ASSOC);
    if (!$order) return [];

    $st = $pdo->prepare("
        SELECT
            soi.id AS order_item_id,
            soi.product_id,
            COALESCE(soi.purchase_order, o.purchase_order, '') AS purchase_order,
            COALESCE(soi.packages_qty, 0) AS packages_qty,
            COALESCE(soi.pallets_qty, 0) AS pallets_qty,
            COALESCE(soi.qty, 0) AS pieces_qty,
            soi.pallet_type,
            soi.pallet_specification,
            p.sku,
            p.name AS product_name,
            COALESCE(ppo.packaging_name, CONCAT('Tarima ', COALESCE(p.packages_per_pallet,0), ' paquetes')) AS packaging_name,
            COALESCE(ppo.packages_per_pallet, p.packages_per_pallet, 0) AS packages_per_pallet,
            COALESCE(ppo.pieces_per_package, p.pieces_per_package, 0) AS pieces_per_package
        FROM sales_order_items soi
        JOIN sales_orders o ON o.id = soi.order_id
        JOIN products p ON p.id = soi.product_id
        LEFT JOIN product_packaging_options ppo
          ON ppo.id = soi.packaging_option_id
         AND ppo.product_id = soi.product_id
         AND ppo.company_id = p.company_id
        WHERE soi.order_id=?
          AND o.company_id=?
          AND o.location_id=?
        ORDER BY COALESCE(soi.purchase_order, o.purchase_order, ''), p.sku, soi.id
    ");
    $st->execute([$orderId, $companyId, $locationId]);
    $items = $st->fetchAll(PDO::FETCH_ASSOC);

    $slots = [];
    $slotNo = 1;
    $customerLabel = trim((string)($order['business_name'] ?: $order['contact_name'] ?: 'Cliente'));
    $deliveryAddress = trim((string)($order['delivery_address'] ?? ''));

    foreach ($items as $it) {
        $pallets = (float)($it['pallets_qty'] ?? 0);
        $packages = (float)($it['packages_qty'] ?? 0);
        $pieces = (float)($it['pieces_qty'] ?? 0);
        $packagesPerPallet = (float)($it['packages_per_pallet'] ?? 0);
        $piecesPerPackage = (float)($it['pieces_per_package'] ?? 0);

        if ($pallets <= 0 && $packages > 0 && $packagesPerPallet > 0) $pallets = $packages / $packagesPerPallet;
        if ($packages <= 0 && $pallets > 0 && $packagesPerPallet > 0) $packages = $pallets * $packagesPerPallet;
        if ($pieces <= 0 && $packages > 0 && $piecesPerPackage > 0) $pieces = $packages * $piecesPerPackage;

        $wholePallets = max(1, (int)round($pallets));
        $packagesPerSlot = $wholePallets > 0 ? ($packages / $wholePallets) : $packages;
        $piecesPerSlot = $wholePallets > 0 ? ($pieces / $wholePallets) : $pieces;
        $productLabel = trim((string)($it['sku'] ?? '') . ' · ' . (string)($it['product_name'] ?? ''));
        if ($productLabel === '·') $productLabel = 'Producto #' . (int)($it['product_id'] ?? 0);

        for ($i = 0; $i < $wholePallets; $i++) {
            $slots[] = [
                'id' => 0,
                'plan_id' => 0,
                'slot_no' => $slotNo++,
                'order_item_id' => (int)($it['order_item_id'] ?? 0),
                'product_id' => (int)($it['product_id'] ?? 0),
                'purchase_order' => trim((string)($it['purchase_order'] ?? '')),
                'customer_label' => $customerLabel,
                'delivery_address' => $deliveryAddress,
                'product_label' => $productLabel,
                'pallets_qty' => 1,
                'packages_qty' => round($packagesPerSlot, 3),
                'pieces_qty' => round($piecesPerSlot, 3),
                'pallet_type' => (string)($it['pallet_type'] ?? ''),
                'pallet_specification' => (string)($it['pallet_specification'] ?? ''),
            ];
        }
    }

    return array_reverse($slots);
}

function botella_virtual_truck_plan_from_order(array $order, array $slots): array {
    $pallets = 0;
    foreach ($slots as $s) $pallets += (float)($s['pallets_qty'] ?? 0);
    $vehicle = trim((string)($order['vehicle_unit'] ?? ''));
    if ($vehicle === '') $vehicle = 'Unidad pendiente por definir';
    return [
        'id' => 0,
        'company_id' => (int)($order['company_id'] ?? 0),
        'location_id' => (int)($order['location_id'] ?? 0),
        'order_id' => (int)($order['id'] ?? 0),
        'vehicle_type' => 'ORDEN_VENTAS',
        'vehicle_label' => $vehicle,
        'capacity_pallets' => $pallets,
        'status' => 'PENDIENTE_ALMACEN',
        'notes' => 'Plan generado automáticamente desde los artículos capturados en Ventas.',
    ];
}

function botella_truck_summary(array $slots): array {
    $out = [];
    foreach ($slots as $s) {
        $po = trim((string)($s['purchase_order'] ?? 'SIN OC')) ?: 'SIN OC';
        $customer = trim((string)($s['customer_label'] ?? 'Cliente no indicado')) ?: 'Cliente no indicado';
        $address = trim((string)($s['delivery_address'] ?? 'Dirección no indicada')) ?: 'Dirección no indicada';
        $product = trim((string)($s['product_label'] ?? ('Producto #' . (int)($s['product_id'] ?? 0))));
        $key = mb_strtoupper($po . '|' . $customer . '|' . $address . '|' . $product, 'UTF-8');
        if (!isset($out[$key])) {
            $out[$key] = [
                'purchase_order'=>$po,
                'customer_label'=>$customer,
                'delivery_address'=>$address,
                'product_label'=>$product,
                'pallets_qty'=>0,
                'packages_qty'=>0,
                'pieces_qty'=>0,
                'slots'=>[],
            ];
        }
        $out[$key]['pallets_qty'] += (float)($s['pallets_qty'] ?? 1);
        $out[$key]['packages_qty'] += (float)($s['packages_qty'] ?? 0);
        $out[$key]['pieces_qty'] += (float)($s['pieces_qty'] ?? 0);
        $out[$key]['slots'][] = (int)($s['slot_no'] ?? 0);
    }
    return array_values($out);
}

function botella_latest_forklift_order(PDO $pdo, int $companyId, int $locationId, int $orderId): ?array {
    if (!table_exists($pdo, 'warehouse_forklift_load_orders')) return null;
    $st = $pdo->prepare("SELECT * FROM warehouse_forklift_load_orders WHERE company_id=? AND location_id=? AND order_id=? ORDER BY id DESC LIMIT 1");
    $st->execute([$companyId, $locationId, $orderId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
}

/* ============================================================
   DETALLE DE BARRA POR ALMACÉN
============================================================ */
function build_bar_detail(PDO $pdo, int $companyId, int $locationId): array {
    $barId = get_bar_product_id($pdo, $companyId);

    if ($barId <= 0) {
        return [
            'bars_by_wh'       => [],
            'bars_internal_wh' => [],
            'pt_products'      => [],
            'bars_used'        => 0.0,
            'bar_commercial'   => 0.0,
            'bar_physical'     => 0.0,
            'note'             => 'No se encontró el producto BARRA 150 KILOS.'
        ];
    }

    $whMap = get_active_warehouses_map($pdo, $companyId, $locationId);
    $stockByWh = build_product_warehouse_stock($pdo, $companyId, $locationId);

    $commercialRows = [];
    $internalRows = [];
    $barCommercial = 0.0;
    $barPhysical = 0.0;

    foreach ($whMap as $w) {
        $wid = (int)$w['id'];
        $code = strtoupper((string)$w['code']);
        $qty = (float)($stockByWh[$barId][$wid] ?? 0.0);

        // No ocultamos inventario interno si es diferente de cero, pero lo separamos
        // para no confundirlo con barra comercial disponible.
        if (abs($qty) <= 0.0001 && !in_array($code, ['CF1','CF2'], true)) {
            continue;
        }

        $row = [
            'warehouse_id' => $wid,
            'code'         => (string)$w['code'],
            'name'         => (string)$w['name'],
            'qty'          => $qty,
        ];

        $barPhysical += $qty;

        if (in_array($code, ['CF1','CF2'], true)) {
            $commercialRows[] = $row;
            $barCommercial += $qty;
        } else {
            $internalRows[] = $row;
        }
    }

    usort($commercialRows, function(array $a, array $b): int {
        return strcmp((string)$a['code'], (string)$b['code']);
    });
    usort($internalRows, function(array $a, array $b): int {
        $order = ['CUB'=>1, 'MERMA'=>2, 'APROV-MERMA'=>3, 'SOB-CUB'=>4];
        $pa = $order[strtoupper((string)$a['code'])] ?? 99;
        $pb = $order[strtoupper((string)$b['code'])] ?? 99;
        if ($pa !== $pb) return $pa <=> $pb;
        return strcmp((string)$a['code'], (string)$b['code']);
    });

    $cf3Id = get_warehouse_id_by_code($pdo, $companyId, $locationId, 'CF3');
    $ptProducts = [];
    $st = $pdo->prepare("\n        SELECT DISTINCT
            p.id,
            p.sku,
            p.name,
            p.uom
        FROM products p
        JOIN product_conversions pc
          ON pc.to_product_id = p.id
         AND pc.company_id = p.company_id
         AND pc.from_product_id = ?
         AND pc.active = 1
        WHERE p.company_id = ?
          AND p.active = 1
        ORDER BY p.sku
    ");
    $st->execute([$barId, $companyId]);
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $p) {
        $pid = (int)$p['id'];
        $p['qty'] = $cf3Id > 0
            ? (float)($stockByWh[$pid][$cf3Id] ?? 0.0)
            : get_product_total_available($pdo, $companyId, $locationId, $pid);
        $ptProducts[] = $p;
    }

    return [
        // Este arreglo ahora contiene SOLO CF1 + CF2 para que coincida con
        // la tarjeta y la tabla principal de inventario disponible comercial.
        'bars_by_wh'       => $commercialRows,
        // Este arreglo muestra existencias reales internas segregadas; no suman
        // al disponible comercial.
        'bars_internal_wh' => $internalRows,
        'pt_products'      => $ptProducts,
        'bars_used'        => 0.0,
        'bar_commercial'   => $barCommercial,
        'bar_physical'     => $barPhysical,
        'note'             => 'La tarjeta principal muestra barra comercial disponible (CF1 + CF2). CUB, MERMA, APROV-MERMA y SOB-CUB se muestran aparte como inventario físico interno y no deben afectar ventas/salidas comerciales.'
    ];
}

/**
 * Define qué almacenes participan en los indicadores de inventario
 * inicial / entradas / salidas / inventario actual.
 *
 * IMPORTANTE:
 * - Para Hielo Barra se restaura la regla funcional original:
 *   BARRA 150 KILOS = CF1 + CF2; producto terminado = CF3.
 *   CUB/SOB-CUB/MERMA pueden verse en el desglose físico, pero no
 *   alteran las tarjetas comerciales del cliente.
 * - Para las demás plantas se respetan sus almacenes activos de
 *   company_id + location_id, sin heredar reglas de Hielo Barra.
 */
function inventory_counts_in_visible_balance(string $sku, string $warehouseCode): bool {
    $code = strtoupper(trim($warehouseCode));

    // Botella: las rutas son almacenes móviles de reparto. No son inventario disponible
    // para nuevas salidas de Almacén/Ventas hasta que exista una devolución real a un almacén físico.
    if (function_exists('botella_is_route_warehouse_code') && botella_is_route_warehouse_code($code)) {
        return false;
    }

    /*
     * Botella / Almacén principal:
     * En esta pantalla NO se deben mezclar movimientos de rutas ni de otros
     * almacenes móviles/operativos como WH-14, WH-16, etc. La revisión de
     * rutas vive en revision_rutas_botella.php.
     * Para el inventario principal de Almacén Botella se muestra únicamente
     * lo que corresponde a ALM01.
     */
    if ((bool)($GLOBALS['isBotellaScope'] ?? false)) {
        return $code === 'ALM01';
    }

    if (!breport_is_hielo_barra_scope()) {
        return true;
    }

    if (is_bar_sku($sku)) {
        return in_array($code, ['CF1','CF2'], true);
    }

    return $code === 'CF3';
}

/* ============================================================
   INVENTARIO OPERATIVO MAESTRO
============================================================ */
function build_inventory_snapshot(PDO $pdo, int $companyId, int $locationId, ?string $stockDateTo = null, ?string $openingDate = null): array {
    /**
     * INVENTARIO OPERATIVO MES POR PLANTA
     * ------------------------------------------------------------
     * Fuente única oficial: inventory_moves.
     *
     * BReport no debe limitarse a CF1/CF2/CF3. Cada empresa/planta puede tener
     * sus propios almacenes (ALM01, CUA, CF1, CUB, SOB-CUB, etc.).
     *
     * Regla:
     * - Se muestran existencias de TODOS los almacenes activos de la planta actual.
     * - Hielo Barra conserva visibilidad de CF1/CF2/CUB/SOB-CUB porque de ahí
     *   surge materia prima y movimientos operativos.
     * - MERMA/APROV-MERMA también quedan visibles si tienen existencia, pero
     *   no se usan automáticamente para surtir ventas.
     */
    $barId = get_bar_product_id($pdo, $companyId);
    $whMap = get_active_warehouses_map($pdo, $companyId, $locationId);

    $stockDateTo = is_string($stockDateTo) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $stockDateTo) ? $stockDateTo : null;
    $openingDate = is_string($openingDate) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $openingDate) ? $openingDate : $stockDateTo;

    $st = $pdo->prepare("
        SELECT id, sku, name, uom
        FROM products
        WHERE company_id = ?
          AND active = 1
          AND NOT (
            UPPER(TRIM(sku)) = 'APROVECHAMIENTO DE MERMA'
            OR UPPER(TRIM(name)) = 'APROVECHAMIENTO DE MERMA'
            OR UPPER(CONCAT(COALESCE(sku,''),' ',COALESCE(name,''))) LIKE '%APROVECHAMIENTO%MERMA%'
          )
        ORDER BY sku
    ");
    $st->execute([$companyId]);
    $products = $st->fetchAll(PDO::FETCH_ASSOC);

    $rows = [];
    foreach ($products as $p) {
        $pid = (int)$p['id'];
        $rows[$pid] = [
            'product_id'        => $pid,
            'sku'               => (string)$p['sku'],
            'name'              => (string)$p['name'],
            'uom'               => (string)$p['uom'],
            'prod_cf1'          => 0.0,
            'prod_cf2'          => 0.0,
            'prod_cub'          => 0.0,
            'prod_total'        => 0.0,
            'cubing_in'         => 0.0,
            'cubing_bars_used'  => 0.0,
            'quality_out'       => 0.0,
            'inventory_initial' => 0.0,
            'manual_in'         => 0.0,
            'manual_out'        => 0.0,
            'on_hand'           => 0.0,
            'warehouse_breakdown' => [],
        ];

        foreach ($whMap as $w) {
            $codeWh = strtoupper(trim((string)($w['code'] ?? '')));
            if (!inventory_counts_in_visible_balance((string)$p['sku'], $codeWh)) {
                continue;
            }
            $rows[$pid]['warehouse_breakdown'][(int)$w['id']] = [
                'warehouse_id' => (int)$w['id'],
                'code' => (string)$w['code'],
                'name' => (string)$w['name'],
                'qty' => 0.0,
                'initial' => 0.0,
                'qty_in' => 0.0,
                'qty_out' => 0.0,
            ];
        }
    }

    $currentWhere = "im.company_id = ? AND im.location_id = ?";
    $currentParams = [$companyId, $locationId];
    if ($stockDateTo !== null) {
        $currentWhere .= " AND im.work_date <= ?";
        $currentParams[] = $stockDateTo;
    }

    $st = $pdo->prepare("
        SELECT
            im.product_id,
            im.warehouse_id,
            UPPER(im.move_type) AS move_type,
            SUM(im.qty) AS qty
        FROM inventory_moves im
        WHERE {$currentWhere}
          AND im.warehouse_id IS NOT NULL
        GROUP BY im.product_id, im.warehouse_id, UPPER(im.move_type)
    ");
    $st->execute($currentParams);

    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
        $pid = (int)$r['product_id'];
        if (!isset($rows[$pid])) continue;

        $wid = (int)($r['warehouse_id'] ?? 0);
        $typ = strtoupper((string)$r['move_type']);
        $qty = (float)$r['qty'];
        $signed = ($typ === 'IN' || $typ === 'ADJ') ? $qty : (($typ === 'OUT') ? -$qty : 0.0);
        $code = strtoupper((string)($whMap[$wid]['code'] ?? ''));
        if (!inventory_counts_in_visible_balance((string)($rows[$pid]['sku'] ?? ''), $code)) {
            continue;
        }

        if (!isset($rows[$pid]['warehouse_breakdown'][$wid])) {
            $rows[$pid]['warehouse_breakdown'][$wid] = [
                'warehouse_id' => $wid,
                'code' => (string)($whMap[$wid]['code'] ?? ('WH-' . $wid)),
                'name' => (string)($whMap[$wid]['name'] ?? 'Almacén'),
                'qty' => 0.0,
                'initial' => 0.0,
                'qty_in' => 0.0,
                'qty_out' => 0.0,
            ];
        }

        /*
         * El desglose conserva la existencia física por almacén para auditoría.
         * El saldo visible de la tarjeta solo suma almacenes permitidos según planta.
         */
        $rows[$pid]['warehouse_breakdown'][$wid]['qty'] += $signed;

        if (!inventory_counts_in_visible_balance((string)$rows[$pid]['sku'], $code)) {
            continue;
        }

        $rows[$pid]['on_hand'] += $signed;
        if ($code === 'CF1') $rows[$pid]['prod_cf1'] += $signed;
        if ($code === 'CF2') $rows[$pid]['prod_cf2'] += $signed;
        if ($code === 'CUB') $rows[$pid]['prod_cub'] += $signed;
        if ($code === 'CF3') $rows[$pid]['cubing_in'] += $signed;
        $rows[$pid]['prod_total'] += $signed;
    }

    if ($openingDate !== null) {
        $openingWhere = "im.company_id = ? AND im.location_id = ? AND im.work_date < ?";
        $openingParams = [$companyId, $locationId, $openingDate];

        $st = $pdo->prepare("
            SELECT
                im.product_id,
                im.warehouse_id,
                UPPER(im.move_type) AS move_type,
                SUM(im.qty) AS qty
            FROM inventory_moves im
            WHERE {$openingWhere}
              AND im.warehouse_id IS NOT NULL
            GROUP BY im.product_id, im.warehouse_id, UPPER(im.move_type)
        ");
        $st->execute($openingParams);

        foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
            $pid = (int)$r['product_id'];
            if (!isset($rows[$pid])) continue;

            $wid = (int)($r['warehouse_id'] ?? 0);
            $typ = strtoupper((string)$r['move_type']);
            $qty = (float)$r['qty'];
            $signed = ($typ === 'IN' || $typ === 'ADJ') ? $qty : (($typ === 'OUT') ? -$qty : 0.0);
            $code = strtoupper((string)($whMap[$wid]['code'] ?? ''));
            if (!inventory_counts_in_visible_balance((string)($rows[$pid]['sku'] ?? ''), $code)) {
                continue;
            }

            /*
             * Siempre conservamos detalle físico del saldo inicial por almacén,
             * pero la tarjeta respeta el alcance operativo/comercial.
             */
            if (isset($rows[$pid]['warehouse_breakdown'][$wid])) {
                $rows[$pid]['warehouse_breakdown'][$wid]['initial'] += $signed;
            }

            if (!inventory_counts_in_visible_balance((string)$rows[$pid]['sku'], $code)) {
                continue;
            }

            $rows[$pid]['inventory_initial'] += $signed;
        }

        $rangeWhere = "im.company_id = ? AND im.location_id = ? AND im.work_date >= ? AND im.work_date <= ?";
        $rangeParams = [$companyId, $locationId, $openingDate, $stockDateTo ?? $openingDate];

        $st = $pdo->prepare("
            SELECT
                im.product_id,
                im.warehouse_id,
                UPPER(im.move_type) AS move_type,
                SUM(im.qty) AS qty
            FROM inventory_moves im
            WHERE {$rangeWhere}
              AND im.warehouse_id IS NOT NULL
            GROUP BY im.product_id, im.warehouse_id, UPPER(im.move_type)
        ");
        $st->execute($rangeParams);

        foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
            $pid = (int)$r['product_id'];
            if (!isset($rows[$pid])) continue;

            $wid = (int)($r['warehouse_id'] ?? 0);
            $typ = strtoupper((string)$r['move_type']);
            $qty = (float)$r['qty'];
            $code = strtoupper((string)($whMap[$wid]['code'] ?? ''));
            if (!inventory_counts_in_visible_balance((string)($rows[$pid]['sku'] ?? ''), $code)) {
                continue;
            }

            /*
             * El detalle por almacén conserva todos los movimientos físicos.
             * Entradas y salidas de las tarjetas se limitan al saldo visible.
             */
            if ($typ === 'IN' || $typ === 'ADJ') {
                if (isset($rows[$pid]['warehouse_breakdown'][$wid])) {
                    $rows[$pid]['warehouse_breakdown'][$wid]['qty_in'] += $qty;
                }
            } elseif ($typ === 'OUT') {
                if (isset($rows[$pid]['warehouse_breakdown'][$wid])) {
                    $rows[$pid]['warehouse_breakdown'][$wid]['qty_out'] += $qty;
                }
            }

            $code = strtoupper((string)($whMap[$wid]['code'] ?? ''));
            if (!inventory_counts_in_visible_balance((string)$rows[$pid]['sku'], $code)) {
                continue;
            }

            if ($typ === 'IN' || $typ === 'ADJ') {
                $rows[$pid]['manual_in'] += $qty;
            } elseif ($typ === 'OUT') {
                $rows[$pid]['manual_out'] += $qty;
            }
        }
    }

    foreach ($rows as &$row) {
        $row['warehouse_breakdown'] = array_values(array_filter($row['warehouse_breakdown'], function(array $w): bool {
            return abs((float)($w['qty'] ?? 0)) > 0.0001
                || abs((float)($w['initial'] ?? 0)) > 0.0001
                || abs((float)($w['qty_in'] ?? 0)) > 0.0001
                || abs((float)($w['qty_out'] ?? 0)) > 0.0001;
        }));
        usort($row['warehouse_breakdown'], function(array $a, array $b): int {
            return strcmp((string)$a['code'], (string)$b['code']);
        });
    }
    unset($row);

    return [
        'rows' => array_values($rows),
        'bar_product_id' => $barId
    ];
}

/* ============================================================
   API
============================================================ */
if ($action !== '') {
    if (!in_array($action, ['sales_dispatch_pdf','moves_export_csv'], true)) {
        header('Content-Type: application/json; charset=utf-8');
    }
    need_csrf();


    
function botella_packaging_for_manual_out(PDO $pdo, int $companyId, int $productId, int $packagingId): array
{
    if ($packagingId > 0) {
        $st = $pdo->prepare("
            SELECT id, product_id, packaging_type, packaging_name,
                   pieces_per_package, packages_per_pallet, active,
                   0 AS legacy_fallback
            FROM product_packaging_options
            WHERE id = ?
              AND company_id = ?
              AND product_id = ?
              AND active = 1
            LIMIT 1
        ");
        $st->execute([$packagingId, $companyId, $productId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if ($row) return $row;
    }

    $st = $pdo->prepare("
        SELECT
            0 AS id,
            id AS product_id,
            'TARIMA' AS packaging_type,
            CONCAT('Embalaje principal · ', packages_per_pallet, ' paq/tar') AS packaging_name,
            pieces_per_package,
            packages_per_pallet,
            active,
            1 AS legacy_fallback
        FROM products
        WHERE id = ?
          AND company_id = ?
          AND active = 1
          AND COALESCE(pieces_per_package,0) > 0
          AND COALESCE(packages_per_pallet,0) > 0
        LIMIT 1
    ");
    $st->execute([$productId, $companyId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) {
        throw new RuntimeException('[LOGISTICS] El artículo no tiene embalaje configurado en Admin.');
    }
    return $row;
}

function botella_calculate_manual_out(array $packaging, float $palletsQtyRaw, string $sku): array
{
    if ($palletsQtyRaw <= 0) {
        throw new RuntimeException('[LOGISTICS] Captura cantidad de tarimas para ' . $sku . '.');
    }
    if (abs($palletsQtyRaw - round($palletsQtyRaw)) > 0.000001) {
        throw new RuntimeException(
            '[PALLET_DECIMAL] La cantidad capturada para ' . $sku .
            ' genera una tarima decimal (' . number_format($palletsQtyRaw, 3, '.', '') . '). No se permiten tarimas decimales.'
        );
    }

    $palletsQty = (int)round($palletsQtyRaw);
    $packagesPerPallet = (int)($packaging['packages_per_pallet'] ?? 0);
    $piecesPerPackage = (int)($packaging['pieces_per_package'] ?? 0);
    if ($packagesPerPallet <= 0 || $piecesPerPackage <= 0) {
        throw new RuntimeException('[LOGISTICS] El embalaje de ' . $sku . ' no cuenta con paquetes/piezas válidas.');
    }

    $packagesQty = $palletsQty * $packagesPerPallet;
    return [
        'pallets_qty' => $palletsQty,
        'packages_qty' => $packagesQty,
        'qty' => $packagesQty * $piecesPerPackage,
        'packages_per_pallet' => $packagesPerPallet,
        'pieces_per_package' => $piecesPerPackage,
    ];
}


if ($action === 'time_debug') {
        $dbNow = null;
        $dbCurDate = null;
        try {
            $st = $pdo->query("SELECT NOW() AS db_now, CURDATE() AS db_date, @@session.time_zone AS db_time_zone");
            $dbRow = $st ? $st->fetch(PDO::FETCH_ASSOC) : null;
            if ($dbRow) {
                $dbNow = (string)($dbRow['db_now'] ?? '');
                $dbCurDate = (string)($dbRow['db_date'] ?? '');
                $dbSessionTz = (string)($dbRow['db_time_zone'] ?? '');
            }
        } catch (Throwable $e) {
            $dbSessionTz = 'No disponible: ' . $e->getMessage();
        }

        jexit([
            'ok' => true,
            'timezone_php' => BREPORT_TIMEZONE,
            'timezone_offset' => breport_timezone_offset(),
            'php_now' => breport_now(),
            'php_today' => breport_today(),
            'operational_today' => breport_operational_today(),
            'operational_shift' => breport_operational_shift(),
            'day_start_hour' => breport_operational_start_hour(),
            'db_now' => $dbNow,
            'db_today' => $dbCurDate,
            'db_session_time_zone' => $dbSessionTz ?? null
        ]);
    }


    if ($action === 'scope_locations') {
        $st = $pdo->prepare("
            SELECT id, name, code, timezone
            FROM locations
            WHERE company_id = ?
              AND active = 1
            ORDER BY name
        ");
        $st->execute([$companyId]);
        jexit([
            'ok' => true,
            'company_id' => $companyId,
            'company_name' => $GLOBALS['scopeCompanyName'] ?? ('Empresa #' . $companyId),
            'current_location_id' => $locationId,
            'current_location_name' => $GLOBALS['scopeLocationName'] ?? ('Localidad #' . $locationId),
            'rows' => $st->fetchAll(PDO::FETCH_ASSOC)
        ]);
    }

    if ($action === 'scope_change') {
        $d = read_json();
        $newLocationId = (int)($d['location_id'] ?? 0);
        if ($newLocationId <= 0) {
            jexit(['ok' => false, 'error' => 'Selecciona una localidad válida.'], 400);
        }

        $st = $pdo->prepare("
            SELECT id, name
            FROM locations
            WHERE id = ?
              AND company_id = ?
              AND active = 1
            LIMIT 1
        ");
        $st->execute([$newLocationId, $companyId]);
        $locRow = $st->fetch(PDO::FETCH_ASSOC);
        if (!$locRow) {
            jexit(['ok' => false, 'error' => 'La localidad seleccionada no pertenece a la empresa actual o no está activa.'], 400);
        }

        /*
         * Se reutiliza el mismo cambio de alcance que usa select_scope.php.
         * El fallback cubre instalaciones donde el helper no esté disponible,
         * sin tocar movimientos ni inventario.
         */
        if (function_exists('set_scope')) {
            set_scope($companyId, $newLocationId);
        } else {
            $_SESSION['company_id'] = $companyId;
            $_SESSION['location_id'] = $newLocationId;
            $_SESSION['scope'] = [
                'company_id' => $companyId,
                'location_id' => $newLocationId,
            ];
            $_SESSION['breport_scope'] = [
                'company_id' => $companyId,
                'location_id' => $newLocationId,
            ];
        }

        jexit([
            'ok' => true,
            'company_id' => $companyId,
            'location_id' => $newLocationId,
            'location_name' => (string)$locRow['name']
        ]);
    }

    if ($action === 'products') {
        $st = $pdo->prepare("
            SELECT id, sku, name, uom
            FROM products
            WHERE company_id = ? AND active = 1
            ORDER BY sku
        ");
        $st->execute([$companyId]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'channels') {
        $st = $pdo->prepare("
            SELECT id, name, channel_type, sort_order
            FROM outbound_channels
            WHERE company_id = ? AND active = 1
            ORDER BY sort_order, name
        ");
        $st->execute([$companyId]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'warehouses') {
        $st = $pdo->prepare("
            SELECT id, code, name
            FROM warehouses
            WHERE company_id = ? AND location_id = ? AND active = 1
            ORDER BY FIELD(code,'CF1','CF2','CF3','CUB','MERMA','APROV-MERMA','SOB-CUB'), name
        ");
        $st->execute([$companyId, $locationId]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }


    if ($action === 'merma_concepts') {
        jexit(['ok' => true, 'rows' => get_merma_concepts($pdo, $companyId)]);
    }

    if ($action === 'locreq_locations') {
        $st = $pdo->prepare("
            SELECT id, name, code
            FROM locations
            WHERE company_id = ?
              AND active = 1
              AND id <> ?
            ORDER BY name
        ");
        $st->execute([$companyId, $locationId]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'locreq_warehouses') {
        $targetLocationId = (int)($_GET['location_id'] ?? 0);
        $productId = (int)($_GET['product_id'] ?? 0);
        $purpose = strtolower(trim((string)($_GET['purpose'] ?? 'source')));
        if (!in_array($purpose, ['source','dest'], true)) $purpose = 'source';
        if ($targetLocationId <= 0 || $productId <= 0) {
            jexit(['ok' => false, 'error' => 'location_id y product_id requeridos'], 400);
        }
        $st = $pdo->prepare("SELECT id FROM locations WHERE id=? AND company_id=? AND active=1 LIMIT 1");
        $st->execute([$targetLocationId, $companyId]);
        if (!$st->fetchColumn()) jexit(['ok'=>false,'error'=>'Localidad inválida.'], 400);

        $rows = locreq_stock_by_allowed_wh($pdo, $companyId, $targetLocationId, $productId, $purpose);
        jexit(['ok' => true, 'rows' => $rows]);
    }

    if ($action === 'locreq_list') {
        $mode = strtolower(trim((string)($_GET['mode'] ?? 'all')));
        $where = ["r.company_id = ?"];
        $params = [$companyId];

        if ($mode === 'created') {
            $where[] = "r.request_location_id = ?";
            $params[] = $locationId;
        } elseif ($mode === 'received') {
            $where[] = "r.source_location_id = ?";
            $params[] = $locationId;
            $where[] = "r.status IN ('PENDIENTE','APROBADA')";
        } elseif ($mode === 'transit') {
            $where[] = "(r.request_location_id = ? OR r.source_location_id = ?)";
            $params[] = $locationId;
            $params[] = $locationId;
            $where[] = "r.status = 'ENVIADA'";
        } elseif ($mode === 'history') {
            $where[] = "(r.request_location_id = ? OR r.source_location_id = ?)";
            $params[] = $locationId;
            $params[] = $locationId;
            $where[] = "r.status IN ('RECIBIDA','RECHAZADA','CANCELADA')";
        } else {
            $where[] = "(r.request_location_id = ? OR r.source_location_id = ?)";
            $params[] = $locationId;
            $params[] = $locationId;
        }

        /*
         * Importante:
         * No agrupamos directo con GROUP BY r.id porque algunas instalaciones MySQL/MariaDB
         * tienen ONLY_FULL_GROUP_BY activo y eso provoca error 500.
         * Primero resumimos los artículos por solicitud en una subconsulta y luego unimos el encabezado.
         */
        $sql = "
            SELECT
                r.id, r.company_id, r.request_location_id, r.source_location_id,
                rl.name AS request_location_name,
                sl.name AS source_location_name,
                r.request_date, r.required_date, r.status,
                r.driver_name, r.vehicle_unit, r.notes, r.created_at,
                r.rejected_at, r.reject_reason, r.approved_at,
                COALESCE(u.full_name, u.username, CONCAT('Usuario #', r.requested_by)) AS requested_by_name,
                COALESCE(ua.full_name, ua.username, CASE WHEN r.approved_by IS NULL THEN '' ELSE CONCAT('Usuario #', r.approved_by) END) AS approved_by_name,
                COALESCE(uj.full_name, uj.username, CASE WHEN r.rejected_by IS NULL THEN '' ELSE CONCAT('Usuario #', r.rejected_by) END) AS rejected_by_name,
                COALESCE(ia.items_count, 0) AS items_count,
                COALESCE(ia.requested_qty_total, 0) AS requested_qty_total,
                COALESCE(ia.shipped_qty_total, 0) AS shipped_qty_total,
                COALESCE(ia.item_summary, '') AS item_summary
            FROM warehouse_location_requests r
            JOIN locations rl ON rl.id = r.request_location_id
            JOIN locations sl ON sl.id = r.source_location_id
            LEFT JOIN users u ON u.id = r.requested_by
            LEFT JOIN users ua ON ua.id = r.approved_by
            LEFT JOIN users uj ON uj.id = r.rejected_by
            LEFT JOIN (
                SELECT
                    i.request_id,
                    COUNT(i.id) AS items_count,
                    COALESCE(SUM(i.requested_qty),0) AS requested_qty_total,
                    COALESCE(SUM(i.shipped_qty),0) AS shipped_qty_total,
                    GROUP_CONCAT(CONCAT(p.sku, ' x ', CAST(ROUND(i.requested_qty, 3) AS CHAR)) ORDER BY p.sku SEPARATOR ' | ') AS item_summary
                FROM warehouse_location_request_items i
                JOIN products p ON p.id = i.product_id
                GROUP BY i.request_id
            ) ia ON ia.request_id = r.id
            WHERE " . implode(' AND ', $where) . "
            ORDER BY r.id DESC
            LIMIT 300
        ";
        $st = $pdo->prepare($sql);
        $st->execute($params);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'locreq_detail') {
        $id = (int)($_GET['id'] ?? 0);
        if ($id <= 0) jexit(['ok'=>false,'error'=>'id requerido'], 400);
        $st = $pdo->prepare("
            SELECT r.*, rl.name AS request_location_name, sl.name AS source_location_name,
                   COALESCE(ur.full_name, ur.username, CONCAT('Usuario #', r.requested_by)) AS requested_by_name,
                   COALESCE(ua.full_name, ua.username, CASE WHEN r.approved_by IS NULL THEN '' ELSE CONCAT('Usuario #', r.approved_by) END) AS approved_by_name,
                   COALESCE(uj.full_name, uj.username, CASE WHEN r.rejected_by IS NULL THEN '' ELSE CONCAT('Usuario #', r.rejected_by) END) AS rejected_by_name,
                   COALESCE(us.full_name, us.username, CASE WHEN r.shipped_by IS NULL THEN '' ELSE CONCAT('Usuario #', r.shipped_by) END) AS shipped_by_name,
                   COALESCE(uc.full_name, uc.username, CASE WHEN r.received_by IS NULL THEN '' ELSE CONCAT('Usuario #', r.received_by) END) AS received_by_name
            FROM warehouse_location_requests r
            JOIN locations rl ON rl.id = r.request_location_id
            JOIN locations sl ON sl.id = r.source_location_id
            LEFT JOIN users ur ON ur.id = r.requested_by
            LEFT JOIN users ua ON ua.id = r.approved_by
            LEFT JOIN users uj ON uj.id = r.rejected_by
            LEFT JOIN users us ON us.id = r.shipped_by
            LEFT JOIN users uc ON uc.id = r.received_by
            WHERE r.id = ? AND r.company_id = ?
              AND (r.request_location_id = ? OR r.source_location_id = ?)
            LIMIT 1
        ");
        $st->execute([$id, $companyId, $locationId, $locationId]);
        $req = $st->fetch(PDO::FETCH_ASSOC);
        if (!$req) jexit(['ok'=>false,'error'=>'Solicitud no encontrada.'], 404);

        $st = $pdo->prepare("
            SELECT i.*, p.sku, p.name AS product_name, p.uom,
                   sw.code AS source_warehouse_code, sw.name AS source_warehouse_name,
                   dw.code AS destination_warehouse_code, dw.name AS destination_warehouse_name
            FROM warehouse_location_request_items i
            JOIN products p ON p.id = i.product_id
            LEFT JOIN warehouses sw ON sw.id = i.source_warehouse_id
            LEFT JOIN warehouses dw ON dw.id = i.destination_warehouse_id
            WHERE i.request_id = ?
            ORDER BY i.id
        ");
        $st->execute([$id]);
        $items = $st->fetchAll(PDO::FETCH_ASSOC);
        foreach ($items as &$it) {
            $it['source_stock'] = locreq_stock_by_allowed_wh($pdo, $companyId, (int)$req['source_location_id'], (int)$it['product_id'], 'source');
            $it['dest_allowed_warehouses'] = locreq_stock_by_allowed_wh($pdo, $companyId, (int)$req['request_location_id'], (int)$it['product_id'], 'dest');
        }
        unset($it);
        jexit(['ok'=>true,'request'=>$req,'items'=>$items]);
    }

    if ($action === 'locreq_create') {
        if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso.'], 403);
        $d = read_json();
        $sourceLocationId = (int)arr_get($d, 'source_location_id', 0);
        $requiredDate = trim((string)arr_get($d, 'required_date', ''));
        $notes = trim((string)arr_get($d, 'notes', ''));
        $items = arr_get($d, 'items', []);
        if ($sourceLocationId <= 0 || $sourceLocationId === $locationId) {
            jexit(['ok'=>false,'error'=>'Selecciona una localidad origen diferente a la actual.'], 400);
        }
        $st = $pdo->prepare("SELECT id FROM locations WHERE id=? AND company_id=? AND active=1 LIMIT 1");
        $st->execute([$sourceLocationId, $companyId]);
        if (!$st->fetchColumn()) jexit(['ok'=>false,'error'=>'Localidad origen inválida.'], 400);
        if (!is_array($items) || !$items) jexit(['ok'=>false,'error'=>'Agrega al menos un artículo.'], 400);
        $clean = [];
        foreach ($items as $it) {
            $pid = (int)($it['product_id'] ?? 0);
            $qty = round((float)($it['qty'] ?? 0), 3);
            if ($pid > 0 && $qty > 0) {
                if (!get_product_row($pdo, $companyId, $pid)) jexit(['ok'=>false,'error'=>'Producto inválido.'], 400);
                $clean[] = ['product_id'=>$pid, 'qty'=>$qty];
            }
        }
        if (!$clean) jexit(['ok'=>false,'error'=>'Artículos inválidos.'], 400);

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("
                INSERT INTO warehouse_location_requests (
                    company_id, request_location_id, source_location_id, request_date, required_date,
                    status, requested_by, notes, created_at
                ) VALUES (?, ?, ?, CURDATE(), ?, 'PENDIENTE', ?, ?, NOW())
            ");
            $st->execute([$companyId, $locationId, $sourceLocationId, ($requiredDate !== '' ? $requiredDate : null), (int)$u['id'], ($notes !== '' ? mb_substr($notes,0,255) : null)]);
            $reqId = (int)$pdo->lastInsertId();
            $ins = $pdo->prepare("
                INSERT INTO warehouse_location_request_items (request_id, product_id, requested_qty, notes)
                VALUES (?, ?, ?, NULL)
            ");
            foreach ($clean as $it) $ins->execute([$reqId, $it['product_id'], $it['qty']]);
            $pdo->commit();
            jexit(['ok'=>true,'id'=>$reqId]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok'=>false,'error'=>$e->getMessage()], 500);
        }
    }

    if ($action === 'locreq_reject') {
        if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso.'], 403);
        $d = read_json();
        $id = (int)arr_get($d, 'id', 0);
        $reason = trim((string)arr_get($d, 'reason', ''));
        if ($id <= 0) jexit(['ok'=>false,'error'=>'id requerido'], 400);
        if ($reason === '') jexit(['ok'=>false,'error'=>'Debes indicar el motivo del rechazo.'], 400);
        $st = $pdo->prepare("
            UPDATE warehouse_location_requests
            SET status='RECHAZADA', rejected_by=?, rejected_at=NOW(), reject_reason=?, updated_at=NOW()
            WHERE id=? AND company_id=? AND source_location_id=? AND status IN ('PENDIENTE','APROBADA')
        ");
        $st->execute([(int)$u['id'], ($reason !== '' ? mb_substr($reason,0,255) : null), $id, $companyId, $locationId]);
        if ($st->rowCount() <= 0) jexit(['ok'=>false,'error'=>'No se pudo rechazar la solicitud.'], 409);
        jexit(['ok'=>true]);
    }

    if ($action === 'locreq_ship') {
        if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso.'], 403);
        $d = read_json();
        $id = (int)arr_get($d, 'id', 0);
        $workDate = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate = breport_resolve_work_date($workDate);
        // Botella: Montacargas solo requiere número de transporte. El chofer ya no se solicita en este flujo.
        $driverName = '';
        $vehicleUnit = trim((string)arr_get($d, 'vehicle_unit', ''));
        $shipItems = arr_get($d, 'items', []);
        if ($id <= 0) jexit(['ok'=>false,'error'=>'id requerido'], 400);
        if (!is_array($shipItems) || !$shipItems) jexit(['ok'=>false,'error'=>'Artículos de envío requeridos.'], 400);

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("SELECT * FROM warehouse_location_requests WHERE id=? AND company_id=? AND source_location_id=? FOR UPDATE");
            $st->execute([$id, $companyId, $locationId]);
            $req = $st->fetch(PDO::FETCH_ASSOC);
            if (!$req) throw new RuntimeException('Solicitud no encontrada para esta localidad origen.');
            if (!in_array((string)$req['status'], ['PENDIENTE','APROBADA'], true)) throw new RuntimeException('La solicitud no está pendiente de envío.');

            $st = $pdo->prepare("SELECT * FROM warehouse_location_request_items WHERE request_id=? FOR UPDATE");
            $st->execute([$id]);
            $dbItems = [];
            foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $row) $dbItems[(int)$row['id']] = $row;
            if (!$dbItems) throw new RuntimeException('La solicitud no tiene artículos.');

            $move = $pdo->prepare("
                INSERT INTO inventory_moves (
                    company_id, location_id, warehouse_id, work_date, product_id, qty, move_type,
                    channel_id, ref_type, ref_id, notes, created_by, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, 'OUT', NULL, 'WH_LOC_TRANSFER_OUT', ?, ?, ?, NOW())
            ");
            $updItem = $pdo->prepare("
                UPDATE warehouse_location_request_items
                SET shipped_qty=?, source_warehouse_id=?, inventory_move_out_id=?
                WHERE id=? AND request_id=?
            ");
            foreach ($shipItems as $it) {
                $itemId = (int)($it['item_id'] ?? 0);
                $qty = round((float)($it['qty'] ?? 0), 3);
                $whId = (int)($it['source_warehouse_id'] ?? 0);
                if (!isset($dbItems[$itemId])) throw new RuntimeException('Artículo de solicitud inválido.');
                $db = $dbItems[$itemId];
                $productId = (int)$db['product_id'];
                $requestedQty = (float)$db['requested_qty'];
                if ($qty <= 0 || $qty > $requestedQty) throw new RuntimeException('Cantidad enviada inválida.');
                locreq_validate_commercial_warehouse($pdo, $companyId, $locationId, $productId, $whId, 'origen', 'source');
                $available = get_product_warehouse_available($pdo, $companyId, $locationId, $productId, $whId);
                if ($qty > $available + 0.0001) throw new RuntimeException('Inventario insuficiente para enviar. Disponible: ' . number_format($available, 3, '.', ''));
                $prod = get_product_row($pdo, $companyId, $productId);
                $note = 'Solicitud loc #' . $id . ' | De ' . locreq_get_location_name($pdo, $locationId) . ' a ' . locreq_get_location_name($pdo, (int)$req['request_location_id']) . ' | SKU: ' . (string)($prod['sku'] ?? '') . ' | Chofer: ' . $driverName . ' | Unidad: ' . $vehicleUnit;
                $move->execute([$companyId, $locationId, $whId, $workDate, $productId, $qty, $id, mb_substr($note,0,255), (int)$u['id']]);
                $moveId = (int)$pdo->lastInsertId();
                $updItem->execute([$qty, $whId, $moveId, $itemId, $id]);
            }
            $st = $pdo->prepare("
                UPDATE warehouse_location_requests
                SET status='ENVIADA', approved_by=COALESCE(approved_by, ?), approved_at=COALESCE(approved_at, NOW()),
                    shipped_by=?, shipped_at=NOW(), driver_name=?, vehicle_unit=?, updated_at=NOW()
                WHERE id=?
            ");
            $st->execute([(int)$u['id'], (int)$u['id'], ($driverName !== '' ? mb_substr($driverName,0,120) : null), ($vehicleUnit !== '' ? mb_substr($vehicleUnit,0,80) : null), $id]);
            $pdo->commit();
            jexit(['ok'=>true]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok'=>false,'error'=>$e->getMessage()], 500);
        }
    }

    if ($action === 'locreq_receive') {
        if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso.'], 403);
        $d = read_json();
        $id = (int)arr_get($d, 'id', 0);
        $workDate = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate = breport_resolve_work_date($workDate);
        $recvItems = arr_get($d, 'items', []);
        if ($id <= 0) jexit(['ok'=>false,'error'=>'id requerido'], 400);
        if (!is_array($recvItems) || !$recvItems) jexit(['ok'=>false,'error'=>'Artículos de recepción requeridos.'], 400);

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("SELECT * FROM warehouse_location_requests WHERE id=? AND company_id=? AND request_location_id=? FOR UPDATE");
            $st->execute([$id, $companyId, $locationId]);
            $req = $st->fetch(PDO::FETCH_ASSOC);
            if (!$req) throw new RuntimeException('Solicitud no encontrada para esta localidad solicitante.');
            if ((string)$req['status'] !== 'ENVIADA') throw new RuntimeException('La solicitud no está en tránsito.');

            $st = $pdo->prepare("SELECT * FROM warehouse_location_request_items WHERE request_id=? FOR UPDATE");
            $st->execute([$id]);
            $dbItems = [];
            foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $row) $dbItems[(int)$row['id']] = $row;

            $move = $pdo->prepare("
                INSERT INTO inventory_moves (
                    company_id, location_id, warehouse_id, work_date, product_id, qty, move_type,
                    channel_id, ref_type, ref_id, notes, created_by, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, 'IN', NULL, 'WH_LOC_TRANSFER_IN', ?, ?, ?, NOW())
            ");
            $updItem = $pdo->prepare("
                UPDATE warehouse_location_request_items
                SET received_qty=?, destination_warehouse_id=?, inventory_move_in_id=?
                WHERE id=? AND request_id=?
            ");
            foreach ($recvItems as $it) {
                $itemId = (int)($it['item_id'] ?? 0);
                $whId = (int)($it['destination_warehouse_id'] ?? 0);
                if (!isset($dbItems[$itemId])) throw new RuntimeException('Artículo de solicitud inválido.');
                $db = $dbItems[$itemId];
                $productId = (int)$db['product_id'];
                $qty = round((float)($db['shipped_qty'] ?? 0), 3);
                if ($qty <= 0) throw new RuntimeException('No existe cantidad enviada para recibir.');
                locreq_validate_commercial_warehouse($pdo, $companyId, $locationId, $productId, $whId, 'destino', 'dest');
                $prod = get_product_row($pdo, $companyId, $productId);
                $note = 'Recepción solicitud loc #' . $id . ' | De ' . locreq_get_location_name($pdo, (int)$req['source_location_id']) . ' a ' . locreq_get_location_name($pdo, $locationId) . ' | SKU: ' . (string)($prod['sku'] ?? '');
                $move->execute([$companyId, $locationId, $whId, $workDate, $productId, $qty, $id, mb_substr($note,0,255), (int)$u['id']]);
                $moveId = (int)$pdo->lastInsertId();
                $updItem->execute([$qty, $whId, $moveId, $itemId, $id]);
            }
            $st = $pdo->prepare("UPDATE warehouse_location_requests SET status='RECIBIDA', received_by=?, received_at=NOW(), updated_at=NOW() WHERE id=?");
            $st->execute([(int)$u['id'], $id]);
            $pdo->commit();
            jexit(['ok'=>true]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok'=>false,'error'=>$e->getMessage()], 500);
        }
    }


    if ($action === 'warehouse_stock') {
        $productId = (int)($_GET['product_id'] ?? 0);
        $warehouseId = (int)($_GET['warehouse_id'] ?? 0);
        if ($productId <= 0 || $warehouseId <= 0) {
            jexit(['ok' => false, 'error' => 'product_id y warehouse_id requeridos'], 400);
        }
        $available = get_product_warehouse_available($pdo, $companyId, $locationId, $productId, $warehouseId);
        jexit(['ok' => true, 'available' => $available]);
    }

    if ($action === 'warehouse_merma_list') {
        $dateFrom = trim((string)($_GET['date_from'] ?? breport_operational_today()));
        $dateTo   = trim((string)($_GET['date_to'] ?? breport_operational_today()));
        $page = max(1, (int)($_GET['page'] ?? 1));
        $perPage = (int)($_GET['per_page'] ?? 10);
        if (!in_array($perPage, [10,25,50,100], true)) $perPage = 10;
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) $dateFrom = breport_operational_today();
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) $dateTo = breport_operational_today();
        if ($dateFrom > $dateTo) { $tmp = $dateFrom; $dateFrom = $dateTo; $dateTo = $tmp; }

        $countSt = $pdo->prepare("\n            SELECT COUNT(*)\n            FROM warehouse_merma_entries wme\n            WHERE wme.company_id = ?\n              AND wme.location_id = ?\n              AND wme.work_date BETWEEN ? AND ?\n        ");
        $countSt->execute([$companyId, $locationId, $dateFrom, $dateTo]);
        $totalRows = (int)$countSt->fetchColumn();
        $totalPages = max(1, (int)ceil($totalRows / max(1, $perPage)));
        if ($page > $totalPages) $page = $totalPages;
        $offset = ($page - 1) * $perPage;

        $st = $pdo->prepare("\n            SELECT\n                wme.id, wme.work_date, wme.product_id, wme.warehouse_id, wme.concept_id, wme.qty, wme.notes, wme.created_at,\n                p.sku, p.name AS product_name, p.uom,\n                w.code AS warehouse_code, w.name AS warehouse_name,\n                qc.name AS concept_name\n            FROM warehouse_merma_entries wme\n            JOIN products p ON p.id = wme.product_id\n            JOIN warehouses w ON w.id = wme.warehouse_id\n            JOIN quality_concepts qc ON qc.id = wme.concept_id\n            WHERE wme.company_id = ?\n              AND wme.location_id = ?\n              AND wme.work_date BETWEEN ? AND ?\n            ORDER BY wme.work_date DESC, wme.id DESC\n            LIMIT $perPage OFFSET $offset\n        ");
        $st->execute([$companyId, $locationId, $dateFrom, $dateTo]);
        jexit([
            'ok' => true,
            'rows' => $st->fetchAll(PDO::FETCH_ASSOC),
            'pagination' => [
                'page'=>$page,
                'per_page'=>$perPage,
                'total_rows'=>$totalRows,
                'total_pages'=>$totalPages,
                'has_prev'=>$page > 1,
                'has_next'=>$page < $totalPages
            ],
            'filters'=>['date_from'=>$dateFrom,'date_to'=>$dateTo]
        ]);
    }

    if ($action === 'warehouse_merma_save') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $d = read_json();
        $workDate = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate = breport_resolve_work_date($workDate);
        $productId = (int)arr_get($d, 'product_id', 0);
        $warehouseId = (int)arr_get($d, 'warehouse_id', 0);
        $conceptId = (int)arr_get($d, 'concept_id', 0);
        $qty = (float)arr_get($d, 'qty', 0);
        $notes = trim((string)arr_get($d, 'notes', ''));

        if ($productId <= 0 || $warehouseId <= 0 || $conceptId <= 0 || $qty <= 0) {
            jexit(['ok' => false, 'error' => 'Fecha, producto, almacén, concepto y cantidad son obligatorios.'], 400);
        }

        $st = $pdo->prepare("\n            SELECT id\n            FROM warehouses\n            WHERE company_id = ? AND location_id = ? AND active = 1 AND id = ?\n            LIMIT 1\n        ");
        $st->execute([$companyId, $locationId, $warehouseId]);
        if (!$st->fetch(PDO::FETCH_ASSOC)) {
            jexit(['ok' => false, 'error' => 'Almacén inválido.'], 400);
        }

        $st = $pdo->prepare("\n            SELECT id\n            FROM products\n            WHERE company_id = ? AND active = 1 AND id = ?\n            LIMIT 1\n        ");
        $st->execute([$companyId, $productId]);
        if (!$st->fetch(PDO::FETCH_ASSOC)) {
            jexit(['ok' => false, 'error' => 'Producto inválido.'], 400);
        }

        $st = $pdo->prepare("\n            SELECT id\n            FROM quality_concepts\n            WHERE company_id = ? AND concept_type = 'MERMA' AND active = 1 AND id = ?\n            LIMIT 1\n        ");
        $st->execute([$companyId, $conceptId]);
        if (!$st->fetch(PDO::FETCH_ASSOC)) {
            jexit(['ok' => false, 'error' => 'Concepto de merma inválido.'], 400);
        }

        try {
            validate_commercial_source_for_merma($pdo, $companyId, $locationId, $productId, $warehouseId);
        } catch (Throwable $e) {
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }

        $available = get_product_warehouse_available($pdo, $companyId, $locationId, $productId, $warehouseId);
        if ($qty > $available) {
            jexit(['ok' => false, 'error' => 'Inventario insuficiente en el almacén seleccionado. Disponible: ' . number_format($available, 3, '.', '')], 409);
        }

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("\n                INSERT INTO quality_entries (\n                    company_id, location_id, work_date, shift, product_id, concept_type, concept_id, qty, notes, created_by, created_at\n                ) VALUES (\n                    ?, ?, ?, 1, ?, 'MERMA', ?, ?, ?, ?, NOW()\n                )\n            ");
            $st->execute([$companyId, $locationId, $workDate, $productId, $conceptId, $qty, ($notes !== '' ? $notes : null), (int)$u['id']]);
            $qualityEntryId = (int)$pdo->lastInsertId();

            $mermaWarehouseId = ensure_merma_warehouse($pdo, $companyId, $locationId);
            if ($mermaWarehouseId <= 0) {
                throw new RuntimeException('No se encontró/creó el almacén MERMA.');
            }

            $sourceCode = get_warehouse_code_by_id($pdo, $companyId, $locationId, $warehouseId);
            $moveNotes = 'Envío a MERMA desde ' . $sourceCode;
            if ($notes !== '') $moveNotes .= ' | ' . $notes;
            $moveNotes = mb_substr($moveNotes, 0, 255);

            $st = $pdo->prepare("\n                INSERT INTO inventory_moves (\n                    company_id, location_id, warehouse_id, work_date, product_id, qty, move_type,\n                    channel_id, ref_type, ref_id, notes, created_by, created_at\n                ) VALUES (\n                    ?, ?, ?, ?, ?, ?, ?,\n                    NULL, 'WH_MERMA_ENVIO', ?, ?, ?, NOW()\n                )\n            ");

            // Movimiento físico real: sale del almacén comercial.
            $st->execute([$companyId, $locationId, $warehouseId, $workDate, $productId, $qty, 'OUT', $qualityEntryId, $moveNotes, (int)$u['id']]);
            $inventoryMoveId = (int)$pdo->lastInsertId();

            // Movimiento físico real: entra al almacén MERMA para disposición de Calidad.
            $st->execute([$companyId, $locationId, $mermaWarehouseId, $workDate, $productId, $qty, 'IN', $qualityEntryId, 'Entrada a MERMA pendiente de disposición | ' . mb_substr($moveNotes, 0, 190), (int)$u['id']]);

            $st = $pdo->prepare("\n                INSERT INTO warehouse_merma_entries (\n                    company_id, location_id, work_date, product_id, warehouse_id, concept_id, qty, notes, quality_entry_id, inventory_move_id, created_by, created_at\n                ) VALUES (\n                    ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()\n                )\n            ");
            $st->execute([$companyId, $locationId, $workDate, $productId, $warehouseId, $conceptId, $qty, ($notes !== '' ? $notes : null), $qualityEntryId, $inventoryMoveId, (int)$u['id']]);

            $pdo->commit();
            jexit(['ok' => true, 'id' => (int)$pdo->lastInsertId()]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok' => false, 'error' => $e->getMessage()], 500);
        }
    }

    if ($action === 'inventory_total') {
        $dateFrom = trim((string)($_GET['date_from'] ?? breport_operational_today()));
        $dateTo   = trim((string)($_GET['date_to'] ?? breport_operational_today()));
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) $dateFrom = breport_operational_today();
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) $dateTo = breport_operational_today();
        if ($dateFrom > $dateTo) { $tmp = $dateFrom; $dateFrom = $dateTo; $dateTo = $tmp; }

        // La existencia Actual de las tarjetas se muestra al cierre de la fecha final del filtro.
        $snap = build_inventory_snapshot($pdo, $companyId, $locationId, $dateTo, $dateFrom);
        $rows = $snap['rows'];
        $barProductId = (int)$snap['bar_product_id'];

        $barTotal = 0.0;
        foreach ($rows as $r) {
            if ((int)$r['product_id'] === $barProductId) {
                $barTotal = (float)$r['on_hand'];
                break;
            }
        }

        jexit([
            'ok'             => true,
            'rows'           => $rows,
            'bar_product_id' => $barProductId,
            'bar_total'      => $barTotal,
            'filters'        => ['date_from' => $dateFrom, 'date_to' => $dateTo],
            'operational'     => [
                'timezone' => BREPORT_TIMEZONE,
                'now' => breport_now(),
                'calendar_today' => breport_today(),
                'operational_today' => breport_operational_today(),
                'shift' => breport_operational_shift(),
                'day_start' => '07:00:00',
                'date_from_start' => breport_operational_window($dateFrom)['start'],
                'date_to_end' => breport_operational_window($dateTo)['end_display'],
            ],
            'stock_cutoff_date' => $dateTo,
            'opening_cutoff_at' => breport_operational_window($dateFrom)['start']
        ]);
    }

    if ($action === 'bars_detail') {
        if (!$isHieloBarraScope) {
            jexit(['ok'=>false, 'error'=>'El detalle especial de barra solo está disponible para la planta Hielo Barra.'], 403);
        }
        $d = build_bar_detail($pdo, $companyId, $locationId);
        jexit(['ok' => true] + $d);
    }

    if ($action === 'moves_list_total') {
        $st = $pdo->prepare("
            SELECT
                im.id,
                im.work_date,
                im.move_type,
                im.qty,
                im.ref_type,
                im.ref_id,
                im.notes,
                im.created_at,
                p.sku,
                p.name AS product_name,
                p.uom,
                COALESCE(w.code, 'SIN-WH') AS warehouse_code,
                COALESCE(w.name, 'Sin almacén') AS warehouse_name,
                oc.name AS channel_name,
                oc.channel_type,
                u.username,
                u.full_name AS user_name
            FROM inventory_moves im
            JOIN products p ON p.id = im.product_id
            LEFT JOIN warehouses w ON w.id = im.warehouse_id
            LEFT JOIN outbound_channels oc ON oc.id = im.channel_id
            LEFT JOIN users u ON u.id = im.created_by
            WHERE im.company_id = ?
              AND im.location_id = ?
              AND UPPER(COALESCE(w.code,'')) = 'ALM01'
            ORDER BY im.id DESC
            LIMIT 250
        ");
        $st->execute([$companyId, $locationId]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }


    if ($action === 'moves_product_summary') {
        $dateFrom = trim((string)($_GET['date_from'] ?? breport_operational_today()));
        $dateTo   = trim((string)($_GET['date_to'] ?? breport_operational_today()));
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) $dateFrom = breport_operational_today();
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) $dateTo = breport_operational_today();
        if ($dateFrom > $dateTo) { $tmp = $dateFrom; $dateFrom = $dateTo; $dateTo = $tmp; }

        $st = $pdo->prepare("
            SELECT
                p.id AS product_id,
                p.sku,
                p.name AS product_name,
                p.uom,
                COALESCE(SUM(CASE WHEN UPPER(im.move_type) IN ('IN','ADJ') THEN im.qty ELSE 0 END), 0) AS qty_in,
                COALESCE(SUM(CASE WHEN UPPER(im.move_type) = 'OUT' THEN im.qty ELSE 0 END), 0) AS qty_out,
                COUNT(im.id) AS moves_count
            FROM inventory_moves im
            JOIN products p ON p.id = im.product_id
            LEFT JOIN warehouses w ON w.id = im.warehouse_id
            WHERE im.company_id = ?
              AND im.location_id = ?
              AND im.work_date BETWEEN ? AND ?
              " . ($isHieloBarraScope ? "
              AND (
                (UPPER(p.sku) = 'BARRA 150 KILOS' AND UPPER(COALESCE(w.code,'')) IN ('CF1','CF2'))
                OR (UPPER(p.sku) <> 'BARRA 150 KILOS' AND UPPER(COALESCE(w.code,'')) = 'CF3')
              )
              " : "
              AND UPPER(COALESCE(w.code,'')) = 'ALM01'
              ") . " 
            GROUP BY p.id, p.sku, p.name, p.uom
            HAVING ABS(qty_in) > 0.0001 OR ABS(qty_out) > 0.0001
            ORDER BY p.sku
        ");
        $st->execute([$companyId, $locationId, $dateFrom, $dateTo]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC), 'date_from'=>$dateFrom, 'date_to'=>$dateTo, 'filter_mode'=>'work_date_operativa']);
    }

    if ($action === 'moves_product_detail') {
        $productId = (int)($_GET['product_id'] ?? 0);
        $direction = strtoupper(trim((string)($_GET['direction'] ?? 'ALL')));
        $dateFrom = trim((string)($_GET['date_from'] ?? breport_operational_today()));
        $dateTo   = trim((string)($_GET['date_to'] ?? breport_operational_today()));
        $page = max(1, (int)($_GET['page'] ?? 1));
        $perPage = (int)($_GET['per_page'] ?? 25);
        if (!in_array($perPage, [10,25,50,100], true)) $perPage = 25;
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) $dateFrom = breport_operational_today();
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) $dateTo = breport_operational_today();
        if ($dateFrom > $dateTo) { $tmp = $dateFrom; $dateFrom = $dateTo; $dateTo = $tmp; }
        if ($productId <= 0) jexit(['ok'=>false,'error'=>'Producto requerido.'], 400);

        $where = ["im.company_id = ?", "im.location_id = ?", "im.product_id = ?", "im.work_date BETWEEN ? AND ?"];
        $params = [$companyId, $locationId, $productId, $dateFrom, $dateTo];
        if ($direction === 'IN') {
            $where[] = "UPPER(im.move_type) IN ('IN','ADJ')";
        } elseif ($direction === 'OUT') {
            $where[] = "UPPER(im.move_type) = 'OUT'";
        } else {
            $direction = 'ALL';
        }
        if ($isHieloBarraScope) {
            // En Hielo Barra el historial comercial de barra conserva CF1 + CF2.
            $where[] = "(UPPER(p.sku) <> 'BARRA 150 KILOS' OR UPPER(COALESCE(w.code,'')) IN ('CF1','CF2'))";
        } else {
            // Botella: el historial de Almacén Botella muestra solo ALM01.
            // Las rutas/almacenes móviles se revisan desde Revisión de rutas.
            $where[] = "UPPER(COALESCE(w.code,'')) = 'ALM01'";
        }
        $whereSql = implode(' AND ', $where);

        $countSt = $pdo->prepare("\n            SELECT COUNT(*)\n            FROM inventory_moves im\n            JOIN products p ON p.id = im.product_id\n            LEFT JOIN warehouses w ON w.id = im.warehouse_id\n            WHERE $whereSql\n        ");
        $countSt->execute($params);
        $totalRows = (int)$countSt->fetchColumn();
        $totalPages = max(1, (int)ceil($totalRows / max(1, $perPage)));
        if ($page > $totalPages) $page = $totalPages;
        $offset = ($page - 1) * $perPage;

        $sql = "
            SELECT
                im.id, im.work_date, im.move_type, im.qty, im.ref_type, im.ref_id, im.notes, im.created_at,
                p.sku, p.name AS product_name, p.uom,
                COALESCE(w.code, 'SIN-WH') AS warehouse_code,
                COALESCE(w.name, 'Sin almacén') AS warehouse_name,
                oc.name AS channel_name,
                COALESCE(u.full_name, u.username, CONCAT('Usuario #', im.created_by)) AS user_name,
                u.username
            FROM inventory_moves im
            JOIN products p ON p.id = im.product_id
            LEFT JOIN warehouses w ON w.id = im.warehouse_id
            LEFT JOIN outbound_channels oc ON oc.id = im.channel_id
            LEFT JOIN users u ON u.id = im.created_by
            WHERE $whereSql
            ORDER BY im.work_date DESC, im.id DESC
            LIMIT $perPage OFFSET $offset
        ";
        $st = $pdo->prepare($sql);
        $st->execute($params);
        jexit([
            'ok'=>true,
            'rows'=>$st->fetchAll(PDO::FETCH_ASSOC),
            'pagination'=>[
                'page'=>$page,
                'per_page'=>$perPage,
                'total_rows'=>$totalRows,
                'total_pages'=>$totalPages,
                'has_prev'=>$page > 1,
                'has_next'=>$page < $totalPages
            ],
            'filters'=>['product_id'=>$productId,'direction'=>$direction,'date_from'=>$dateFrom,'date_to'=>$dateTo]
        ]);
    }


    if ($action === 'moves_export_csv') {
        $dateFrom = trim((string)($_GET['date_from'] ?? breport_operational_today()));
        $dateTo   = trim((string)($_GET['date_to'] ?? breport_operational_today()));
        $direction = strtoupper(trim((string)($_GET['direction'] ?? 'ALL')));
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) $dateFrom = breport_operational_today();
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) $dateTo = breport_operational_today();
        if ($dateFrom > $dateTo) { $tmp = $dateFrom; $dateFrom = $dateTo; $dateTo = $tmp; }

        $where = ["im.company_id = ?", "im.location_id = ?", "im.work_date BETWEEN ? AND ?"];
        $params = [$companyId, $locationId, $dateFrom, $dateTo];
        if ($direction === 'IN') {
            $where[] = "UPPER(im.move_type) IN ('IN','ADJ')";
        } elseif ($direction === 'OUT') {
            $where[] = "UPPER(im.move_type) = 'OUT'";
        } else {
            $direction = 'ALL';
        }
        if ($isHieloBarraScope) {
            // Exportación comercial especial exclusivamente para Hielo Barra.
            $where[] = "((UPPER(p.sku) = 'BARRA 150 KILOS' AND UPPER(COALESCE(w.code,'')) IN ('CF1','CF2')) OR (UPPER(p.sku) <> 'BARRA 150 KILOS' AND UPPER(COALESCE(w.code,'')) = 'CF3'))";
        } else {
            // Botella: exportar únicamente movimientos del almacén principal ALM01.
            $where[] = "UPPER(COALESCE(w.code,'')) = 'ALM01'";
        }
        $whereSql = implode(' AND ', $where);

        $st = $pdo->prepare("
            SELECT
                im.work_date,
                im.created_at,
                UPPER(im.move_type) AS move_type,
                p.sku,
                p.name AS product_name,
                p.uom,
                COALESCE(w.code, 'SIN-WH') AS warehouse_code,
                COALESCE(w.name, 'Sin almacén') AS warehouse_name,
                im.qty,
                im.ref_type,
                im.ref_id,
                COALESCE(u.full_name, u.username, CONCAT('Usuario #', im.created_by)) AS user_name,
                im.notes
            FROM inventory_moves im
            JOIN products p ON p.id = im.product_id
            LEFT JOIN warehouses w ON w.id = im.warehouse_id
            LEFT JOIN users u ON u.id = im.created_by
            WHERE $whereSql
            ORDER BY p.sku, im.work_date, im.created_at, im.id
        ");
        $st->execute($params);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);

        $filename = 'historial_movimientos_' . $dateFrom . '_a_' . $dateTo . '.csv';
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        $out = fopen('php://output', 'w');
        fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF));
        fputcsv($out, ['Fecha','Hora','Tipo','SKU','Producto','Almacen','Almacen nombre','Cantidad','UOM','Referencia','Ref ID','Usuario','Notas']);
        foreach ($rows as $r) {
            $created = (string)($r['created_at'] ?? '');
            $hour = $created !== '' ? substr($created, 11, 8) : '';
            $type = strtoupper((string)($r['move_type'] ?? '')) === 'OUT' ? 'SALIDA' : 'ENTRADA';
            fputcsv($out, [
                (string)($r['work_date'] ?? ''),
                $hour,
                $type,
                (string)($r['sku'] ?? ''),
                (string)($r['product_name'] ?? ''),
                (string)($r['warehouse_code'] ?? ''),
                (string)($r['warehouse_name'] ?? ''),
                number_format((float)($r['qty'] ?? 0), 3, '.', ''),
                (string)($r['uom'] ?? ''),
                (string)($r['ref_type'] ?? ''),
                (string)($r['ref_id'] ?? ''),
                (string)($r['user_name'] ?? ''),
                (string)($r['notes'] ?? ''),
            ]);
        }
        fclose($out);
        exit;
    }



    if ($action === 'botella_load_plan_detail') {
        $orderId = (int)($_GET['order_id'] ?? 0);
        if ($orderId <= 0) jexit(['ok'=>false,'error'=>'order_id requerido'], 400);
        $stOrder = $pdo->prepare("SELECT * FROM sales_orders WHERE id=? AND company_id=? AND location_id=? LIMIT 1");
        $stOrder->execute([$orderId, $companyId, $locationId]);
        $orderForPlan = $stOrder->fetch(PDO::FETCH_ASSOC);
        if (!$orderForPlan) jexit(['ok'=>false,'error'=>'Pedido no encontrado.'], 404);

        $plan = botella_truck_plan_for_order($pdo, $companyId, $locationId, $orderId);
        if ($plan) {
            $slots = botella_truck_plan_slots($pdo, (int)$plan['id']);
            $printUrl = 'carga_camion_botella.php?action=print&plan_id='.(int)$plan['id'];
        } else {
            $slots = botella_order_items_as_truck_slots($pdo, $companyId, $locationId, $orderId);
            if (!$slots) jexit(['ok'=>false,'error'=>'La orden no tiene artículos/tarimas registrados para enviar a montacargas.'], 404);
            $plan = botella_virtual_truck_plan_from_order($orderForPlan, $slots);
            $printUrl = '';
        }
        $forklift = botella_latest_forklift_order($pdo, $companyId, $locationId, $orderId);
        jexit([
            'ok'=>true,
            'plan'=>$plan,
            'slots'=>$slots,
            'summary'=>botella_truck_summary($slots),
            'forklift_order'=>$forklift,
            'print_url'=>$printUrl,
        ]);
    }

    if ($action === 'botella_create_forklift_order') {
        if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso.'], 403);
        $d = read_json();
        $orderId = (int)arr_get($d, 'order_id', 0);
        $vehicleUnit = trim((string)arr_get($d, 'vehicle_unit', ''));
        // Botella: Montacargas solo requiere número de transporte. El chofer ya no se solicita en este flujo.
        $driverName = '';
        $notes = trim((string)arr_get($d, 'notes', ''));
        if ($orderId <= 0) jexit(['ok'=>false,'error'=>'order_id requerido'], 400);
        if ($vehicleUnit === '') jexit(['ok'=>false,'error'=>'Indica número de unidad a cargar.'], 400);

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("SELECT * FROM sales_orders WHERE id=? AND company_id=? AND location_id=? FOR UPDATE");
            $st->execute([$orderId, $companyId, $locationId]);
            $order = $st->fetch(PDO::FETCH_ASSOC);
            if (!$order) throw new RuntimeException('Pedido no encontrado.');
            if ((string)$order['status'] !== 'AUTORIZADA' || !empty($order['outbound_id'])) {
                throw new RuntimeException('Solo se puede enviar a montacargas una orden AUTORIZADA pendiente de salida.');
            }
            $plan = botella_truck_plan_for_order($pdo, $companyId, $locationId, $orderId);
            if ($plan) {
                $slots = botella_truck_plan_slots($pdo, (int)$plan['id']);
                $truckPlanId = (int)$plan['id'];
            } else {
                $slots = botella_order_items_as_truck_slots($pdo, $companyId, $locationId, $orderId);
                $truckPlanId = 0;
            }
            if (!$slots) throw new RuntimeException('La orden no tiene tarimas/artículos asignados para enviar a montacargas.');
            $latest = botella_latest_forklift_order($pdo, $companyId, $locationId, $orderId);
            if ($latest && !in_array(strtoupper((string)$latest['status']), ['CANCELADA','COMPLETA'], true)) {
                throw new RuntimeException('Ya existe una orden de carga a montacargas pendiente o en proceso.');
            }
            $st = $pdo->prepare("INSERT INTO warehouse_forklift_load_orders(company_id, location_id, order_id, truck_plan_id, status, vehicle_unit, driver_name, notes, created_by, created_at) VALUES(?,?,?,?, 'PENDIENTE', ?, ?, ?, ?, NOW())");
            $st->execute([$companyId, $locationId, $orderId, $truckPlanId, $vehicleUnit, $driverName, $notes !== '' ? $notes : null, (int)$u['id']]);
            $forkliftId = (int)$pdo->lastInsertId();
            $stItem = $pdo->prepare("INSERT INTO warehouse_forklift_load_order_items(forklift_order_id, slot_no, product_id, purchase_order, customer_label, delivery_address, product_label, pallets_qty, packages_qty, pieces_qty) VALUES(?,?,?,?,?,?,?,?,?,?)");
            foreach ($slots as $slt) {
                $stItem->execute([$forkliftId, (int)$slt['slot_no'], (int)$slt['product_id'], $slt['purchase_order'] ?? null, $slt['customer_label'] ?? null, $slt['delivery_address'] ?? null, $slt['product_label'] ?? null, (float)($slt['pallets_qty'] ?? 1), (float)($slt['packages_qty'] ?? 0), (float)($slt['pieces_qty'] ?? 0)]);
            }
            if ($truckPlanId > 0) {
                $pdo->prepare("UPDATE sales_truck_load_plans SET status='SENT_TO_FORKLIFT', updated_by=?, updated_at=NOW() WHERE id=?")->execute([(int)$u['id'], $truckPlanId]);
            }
            $pdo->prepare("UPDATE sales_orders SET status='SURTIENDO', updated_at=NOW() WHERE id=? AND company_id=? AND location_id=?")->execute([$orderId, $companyId, $locationId]);
            if (column_exists($pdo, 'sales_order_status_log', 'changed_by_user_id')) {
                $st = $pdo->prepare("INSERT INTO sales_order_status_log(order_id, old_status, new_status, changed_by_user_id, changed_by_role, comment, created_at) VALUES(?, ?, ?, ?, ?, ?, NOW())");
                $logComment = 'Orden de carga enviada a montacargas #' . $forkliftId . ' | Transporte ' . $vehicleUnit;
                $st->execute([$orderId, (string)$order['status'], 'SURTIENDO', (int)$u['id'], 'ALMACEN', $logComment]);
            }
            $pdo->commit();
            jexit(['ok'=>true,'forklift_order_id'=>$forkliftId]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok'=>false,'error'=>$e->getMessage()], 400);
        }
    }

    /*
     * BANDEJA OPERATIVA DE SALIDAS
     * ------------------------------------------------------------
     * Consulta solamente:
     * - Ordenes de ventas creadas en BReport.
     * - Salidas manuales generadas desde almacén.
     * VentasHielo continúa consumiéndose por su API existente
     * vh_authorized / vh_order_detail para no alterar el bridge.
     */

    if ($action === 'dispatch_pending_counter') {
        $where = [
            "o.company_id = ?",
            "o.location_id = ?",
            "(UPPER(COALESCE(o.source,'')) = 'VENTAS' OR o.sales_order_ref LIKE 'OSV-%' OR o.sales_order_ref LIKE 'OSA-%')",
            "UPPER(COALESCE(o.status,'')) = 'AUTORIZADA'",
            "o.outbound_id IS NULL"
        ];
        $params = [$companyId, $locationId];

        $st = $pdo->prepare("
            SELECT
                COUNT(*) AS pending_count,
                COALESCE(MAX(o.id), 0) AS last_order_id,
                COALESCE(MAX(o.updated_at), MAX(o.created_at)) AS last_activity
            FROM sales_orders o
            WHERE " . implode(' AND ', $where) . "
        ");
        $st->execute($params);
        $row = $st->fetch(PDO::FETCH_ASSOC) ?: [];

        $st = $pdo->prepare("
            SELECT
                o.id,
                o.sales_order_ref,
                o.requested_delivery_date,
                o.purchase_order,
                c.business_name,
                c.contact_name
            FROM sales_orders o
            JOIN sales_customers c ON c.id = o.customer_id
            WHERE " . implode(' AND ', $where) . "
            ORDER BY o.id DESC
            LIMIT 5
        ");
        $st->execute($params);

        jexit([
            'ok' => true,
            'pending_count' => (int)($row['pending_count'] ?? 0),
            'last_order_id' => (int)($row['last_order_id'] ?? 0),
            'last_activity' => (string)($row['last_activity'] ?? ''),
            'rows' => $st->fetchAll(PDO::FETCH_ASSOC),
            'checked_at' => breport_now(),
        ]);
    }

    if ($action === 'dispatch_mailbox_sales') {
        $date = trim((string)($_GET['date'] ?? ''));
        $where = [
            "o.company_id = ?",
            "o.location_id = ?",
            "(UPPER(COALESCE(o.source,'')) = 'VENTAS' OR o.sales_order_ref LIKE 'OSV-%')",
            "UPPER(COALESCE(o.status,'')) NOT IN ('CANCELADA','CANCELADO','ELIMINADA','ELIMINADO')"
        ];
        $params = [$companyId, $locationId];

        if ($date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            $where[] = "o.requested_delivery_date = ?";
            $params[] = $date;
        }

        $hasRouteNo = column_exists($pdo, 'sales_orders', 'route_no');
        $routeSelect = $hasRouteNo ? "o.route_no" : "NULL AS route_no";
        $orderPurchaseSelect = column_exists($pdo, 'sales_orders', 'purchase_order') ? "o.purchase_order" : "NULL";
        $purchaseOrderSelect = $orderPurchaseSelect . " AS purchase_order";
        if (table_exists($pdo, 'sales_order_items') && column_exists($pdo, 'sales_order_items', 'purchase_order')) {
            /*
             * Una orden OSV puede contener varias OC reales por renglón.
             * La bandeja de Almacén no debe tomar solo sales_orders.purchase_order,
             * porque ese campo puede ser una OC cabecera/primaria y provoca que
             * todos los artículos se vean con la misma OC.
             */
            $purchaseOrderSelect = "COALESCE(NULLIF((SELECT GROUP_CONCAT(DISTINCT NULLIF(TRIM(soi_po.purchase_order),'') ORDER BY soi_po.purchase_order SEPARATOR ' / ') FROM sales_order_items soi_po WHERE soi_po.order_id = o.id), ''), {$orderPurchaseSelect}) AS purchase_order";
        }

        $st = $pdo->prepare("
            SELECT
                o.id,
                o.sales_order_ref,
                {$routeSelect},
                o.requested_delivery_date,
                o.requested_delivery_time,
                o.delivery_address,
                {$purchaseOrderSelect},
                o.comments,
                o.status,
                o.source,
                o.outbound_id,
                o.fulfilled_at,
                (SELECT fo.status FROM warehouse_forklift_load_orders fo WHERE fo.company_id=o.company_id AND fo.location_id=o.location_id AND fo.order_id=o.id ORDER BY fo.id DESC LIMIT 1) AS forklift_status,
                (SELECT fo.id FROM warehouse_forklift_load_orders fo WHERE fo.company_id=o.company_id AND fo.location_id=o.location_id AND fo.order_id=o.id ORDER BY fo.id DESC LIMIT 1) AS forklift_order_id,
                c.business_name,
                c.contact_name,
                c.phone,
                (
                    SELECT COUNT(*)
                    FROM sales_order_items soi
                    WHERE soi.order_id = o.id
                ) AS items_count,
                (
                    SELECT COALESCE(SUM(soi.qty),0)
                    FROM sales_order_items soi
                    WHERE soi.order_id = o.id
                ) AS total_qty
            FROM sales_orders o
            JOIN sales_customers c ON c.id = o.customer_id
            WHERE " . implode(' AND ', $where) . "
            ORDER BY COALESCE(o.requested_delivery_date, '9999-12-31') ASC, o.id ASC
            LIMIT 300
        ");
        $st->execute($params);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);
        $rows = botella_enrich_sales_dispatch_stock($pdo, $companyId, $locationId, $rows);
        jexit(['ok'=>true, 'rows'=>$rows, 'date'=>$date]);
    }

    if ($action === 'dispatch_mailbox_outbounds') {
        $date = trim((string)($_GET['date'] ?? ''));
        $where = [
            "ob.company_id = ?",
            "ob.location_id = ?",
            "NOT EXISTS (
                SELECT 1
                FROM sales_orders so
                WHERE so.company_id = ob.company_id
                  AND so.location_id = ob.location_id
                  AND so.outbound_id = ob.id
            )",
            "(ob.reference IS NULL OR (
                UPPER(ob.reference) NOT LIKE 'OSV-%'
                AND UPPER(ob.reference) NOT LIKE 'PW-%'
            ))"
        ];
        $params = [$companyId, $locationId];

        if ($date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            $where[] = "ob.outbound_date = ?";
            $params[] = $date;
        }

        $st = $pdo->prepare("
            SELECT
                ob.id,
                ob.outbound_date,
                ob.reference,
                ob.status,
                ob.notes,
                oc.name AS channel_name,
                COALESCE(u.full_name, u.username, CONCAT('Usuario #', ob.created_by)) AS user_name,
                COUNT(oi.product_id) AS items_count,
                COALESCE(SUM(oi.qty),0) AS total_qty
            FROM outbounds ob
            LEFT JOIN outbound_channels oc ON oc.id = ob.channel_id
            LEFT JOIN users u ON u.id = ob.created_by
            LEFT JOIN outbound_items oi ON oi.outbound_id = ob.id
            WHERE " . implode(' AND ', $where) . "
            GROUP BY
                ob.id, ob.outbound_date, ob.reference, ob.status, ob.notes,
                oc.name, u.full_name, u.username, ob.created_by
            ORDER BY ob.outbound_date DESC, ob.id DESC
            LIMIT 300
        ");
        $st->execute($params);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);
        foreach ($rows as &$rr) {
            $routeText = implode(' | ', array_filter([
                (string)($rr['reference'] ?? ''),
                (string)($rr['notes'] ?? ''),
                (string)($rr['channel_name'] ?? '')
            ]));
            $routeNoParsed = 0;
            if (preg_match('/RUTA[-_\s#:]*(\d+)/i', $routeText, $m)) {
                $routeNoParsed = (int)$m[1];
            } elseif (preg_match('/RUTA\s*DLX[:\s#-]*(\d+)/i', $routeText, $m)) {
                $routeNoParsed = (int)$m[1];
            } elseif (preg_match('/CARGA\s+A\s+RUTA[:\s#-]*(\d+)/i', $routeText, $m)) {
                $routeNoParsed = (int)$m[1];
            }
            $rr['route_no'] = $routeNoParsed > 0 ? $routeNoParsed : null;
            $rr['is_route_load'] = $routeNoParsed > 0;
        }
        unset($rr);
        jexit(['ok'=>true, 'rows'=>$rows, 'date'=>$date]);
    }

    if ($action === 'dispatch_mailbox_outbound_detail') {
        $outboundId = (int)($_GET['outbound_id'] ?? 0);
        if ($outboundId <= 0) {
            jexit(['ok'=>false, 'error'=>'outbound_id requerido'], 400);
        }

        $st = $pdo->prepare("
            SELECT
                ob.id, ob.outbound_date, ob.reference, ob.status, ob.notes,
                oc.name AS channel_name,
                COALESCE(u.full_name, u.username, CONCAT('Usuario #', ob.created_by)) AS user_name
            FROM outbounds ob
            LEFT JOIN outbound_channels oc ON oc.id = ob.channel_id
            LEFT JOIN users u ON u.id = ob.created_by
            WHERE ob.id=?
              AND ob.company_id=?
              AND ob.location_id=?
            LIMIT 1
        ");
        $st->execute([$outboundId, $companyId, $locationId]);
        $outbound = $st->fetch(PDO::FETCH_ASSOC);
        if (!$outbound) {
            jexit(['ok'=>false, 'error'=>'Salida no encontrada en la planta activa.'], 404);
        }

        $st = $pdo->prepare("
            SELECT oi.product_id, oi.qty, p.sku, p.name AS product_name, p.uom
            FROM outbound_items oi
            JOIN products p ON p.id = oi.product_id
            WHERE oi.outbound_id=?
            ORDER BY p.sku
        ");
        $st->execute([$outboundId]);
        jexit(['ok'=>true, 'outbound'=>$outbound, 'items'=>$st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'sales_authorized_breport') {
        $source = strtoupper(trim((string)($_GET['source'] ?? 'VENTAS')));

        $where = [
            "o.company_id = ?",
            "o.location_id = ?",
            "o.status = 'AUTORIZADA'",
            "o.outbound_id IS NULL"
        ];
        $params = [$companyId, $locationId];

        if ($source === 'VENTAS') {
            $where[] = "(UPPER(COALESCE(o.source,'')) = 'VENTAS' OR o.sales_order_ref LIKE 'OSV-%')";
        } elseif ($source === 'VENTASHIELO') {
            $where[] = "(UPPER(COALESCE(o.source,'')) IN ('VENTASHIELO','VH') OR o.sales_order_ref LIKE 'PW-%')";
        }

        $st = $pdo->prepare("
            SELECT
                o.id,
                o.sales_order_ref,
                o.requested_delivery_date,
                o.requested_delivery_time,
                o.delivery_address,
                o.comments,
                o.total,
                o.status,
                o.source,
                o.approved_at,
                o.outbound_id,
                c.business_name,
                c.contact_name,
                c.phone,
                (
                    SELECT COUNT(*)
                    FROM sales_order_items soi
                    WHERE soi.order_id = o.id
                ) AS items_count,
                (
                    SELECT COALESCE(SUM(soi.qty),0)
                    FROM sales_order_items soi
                    WHERE soi.order_id = o.id
                ) AS total_qty
            FROM sales_orders o
            JOIN sales_customers c
              ON c.id = o.customer_id
            WHERE " . implode(' AND ', $where) . "
            ORDER BY o.id DESC
            LIMIT 300
        ");
        $st->execute($params);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }


    if ($action === 'sales_route_orders') {
        $date = trim((string)($_GET['date'] ?? breport_operational_today()));
        if ($date === '') $date = breport_operational_today();

        $hasRouteNo = column_exists($pdo, 'sales_orders', 'route_no');
        $routeSelect = $hasRouteNo ? "o.route_no" : "NULL AS route_no";
        $routeOrder = $hasRouteNo ? "COALESCE(o.route_no, 999), o.id DESC" : "o.id DESC";

        $st = $pdo->prepare("
            SELECT
                o.id,
                o.sales_order_ref,
                {$routeSelect},
                o.requested_delivery_date,
                o.requested_delivery_time,
                o.delivery_address,
                o.comments,
                o.total,
                o.status,
                o.source,
                o.approved_at,
                o.outbound_id,
                o.fulfilled_at,
                (SELECT fo.status FROM warehouse_forklift_load_orders fo WHERE fo.company_id=o.company_id AND fo.location_id=o.location_id AND fo.order_id=o.id ORDER BY fo.id DESC LIMIT 1) AS forklift_status,
                (SELECT fo.id FROM warehouse_forklift_load_orders fo WHERE fo.company_id=o.company_id AND fo.location_id=o.location_id AND fo.order_id=o.id ORDER BY fo.id DESC LIMIT 1) AS forklift_order_id,
                c.business_name,
                c.contact_name,
                c.phone,
                (
                    SELECT COUNT(*)
                    FROM sales_order_items soi
                    WHERE soi.order_id = o.id
                ) AS items_count,
                (
                    SELECT COALESCE(SUM(soi.qty),0)
                    FROM sales_order_items soi
                    WHERE soi.order_id = o.id
                ) AS total_qty
            FROM sales_orders o
            JOIN sales_customers c
              ON c.id = o.customer_id
            WHERE o.company_id = ?
              AND o.location_id = ?
              AND o.requested_delivery_date = ?
              AND (UPPER(COALESCE(o.source,'')) = 'VENTAS' OR o.sales_order_ref LIKE 'OSV-%')
              AND UPPER(COALESCE(o.status,'')) NOT IN ('CANCELADA','CANCELADO','ELIMINADA','ELIMINADO')
            ORDER BY {$routeOrder}
            LIMIT 500
        ");
        $st->execute([$companyId, $locationId, $date]);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);

        $summary = [];
        foreach ($rows as $r) {
            $route = (int)($r['route_no'] ?? 0);
            if ($route <= 0) $route = 0;
            if (!isset($summary[$route])) {
                $summary[$route] = [
                    'route_no' => $route,
                    'orders_count' => 0,
                    'pending_count' => 0,
                    'dispatched_count' => 0,
                    'items_count' => 0,
                    'total_qty' => 0.0,
                    'orders' => []
                ];
            }
            $isDispatched = !empty($r['outbound_id']) || strtoupper((string)$r['status']) !== 'AUTORIZADA';
            $summary[$route]['orders_count']++;
            $summary[$route]['items_count'] += (int)($r['items_count'] ?? 0);
            $summary[$route]['total_qty'] += (float)($r['total_qty'] ?? 0);
            if ($isDispatched) $summary[$route]['dispatched_count']++;
            else $summary[$route]['pending_count']++;
            $summary[$route]['orders'][] = $r;
        }

        ksort($summary);
        jexit(['ok' => true, 'date' => $date, 'routes' => array_values($summary), 'rows' => $rows]);
    }

    if ($action === 'sales_authorized') {
        $st = $pdo->prepare("
            SELECT
                o.id,
                o.sales_order_ref,
                o.requested_delivery_date,
                o.requested_delivery_time,
                o.delivery_address,
                o.comments,
                o.total,
                o.status,
                o.approved_at,
                o.outbound_id,
                c.business_name,
                c.contact_name,
                c.phone,
                (
                    SELECT COUNT(*)
                    FROM sales_order_items soi
                    WHERE soi.order_id = o.id
                ) AS items_count
            FROM sales_orders o
            JOIN sales_customers c
              ON c.id = o.customer_id
            WHERE o.company_id = ?
              AND o.location_id = ?
              AND o.status = 'AUTORIZADA'
              AND o.outbound_id IS NULL
            ORDER BY o.id DESC
            LIMIT 300
        ");
        $st->execute([$companyId, $locationId]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'sales_order_detail') {
        $orderId = (int)($_GET['order_id'] ?? 0);
        if ($orderId <= 0) {
            jexit(['ok' => false, 'error' => 'order_id requerido'], 400);
        }

        $st = $pdo->prepare("
            SELECT
                o.*,
                c.business_name,
                c.contact_name,
                c.phone
            FROM sales_orders o
            JOIN sales_customers c
              ON c.id = o.customer_id
            WHERE o.id = ?
              AND o.company_id = ?
              AND o.location_id = ?
            LIMIT 1
        ");
        $st->execute([$orderId, $companyId, $locationId]);
        $order = $st->fetch(PDO::FETCH_ASSOC);

        if (!$order) {
            jexit(['ok' => false, 'error' => 'Pedido no encontrado'], 404);
        }

        /*
         * Detalle robusto de artículos Botella.
         * Algunas instalaciones tienen dumps anteriores donde todavía no existen
         * todas las columnas nuevas de sales_order_items o la tabla de embalajes.
         * Para evitar error 500 / respuesta no JSON, las columnas opcionales se
         * consultan de forma dinámica y se regresan con valor por defecto.
         */
        $itemSelect = [
            'i.id',
            'i.product_id',
            (column_exists($pdo, 'sales_order_items', 'purchase_order') ? 'i.purchase_order' : 'NULL AS purchase_order'),
            (column_exists($pdo, 'sales_order_items', 'oc_relation') ? 'i.oc_relation' : 'NULL AS oc_relation'),
            (column_exists($pdo, 'sales_order_items', 'item_customer_id') ? 'i.item_customer_id' : 'NULL AS item_customer_id'),
            (column_exists($pdo, 'sales_order_items', 'item_address_id') ? 'i.item_address_id' : 'NULL AS item_address_id'),
            (column_exists($pdo, 'sales_order_items', 'customer_label') ? 'i.customer_label' : 'NULL AS customer_label'),
            (column_exists($pdo, 'sales_order_items', 'delivery_address') ? 'i.delivery_address AS item_delivery_address' : 'NULL AS item_delivery_address'),
            (column_exists($pdo, 'sales_order_items', 'oc_attachment_missing_reason') ? 'i.oc_attachment_missing_reason' : 'NULL AS oc_attachment_missing_reason'),
            (column_exists($pdo, 'sales_order_items', 'packaging_option_id') ? 'i.packaging_option_id' : 'NULL AS packaging_option_id'),
            'i.qty',
            (column_exists($pdo, 'sales_order_items', 'packages_qty') ? 'i.packages_qty' : '0 AS packages_qty'),
            (column_exists($pdo, 'sales_order_items', 'pallets_qty') ? 'i.pallets_qty' : '0 AS pallets_qty'),
            (column_exists($pdo, 'sales_order_items', 'pallet_type') ? 'i.pallet_type' : 'NULL AS pallet_type'),
            (column_exists($pdo, 'sales_order_items', 'pallet_specification') ? 'i.pallet_specification' : 'NULL AS pallet_specification'),
            (column_exists($pdo, 'sales_order_items', 'unit_price') ? 'i.unit_price' : '0 AS unit_price'),
            (column_exists($pdo, 'sales_order_items', 'line_total') ? 'i.line_total' : '0 AS line_total'),
            (column_exists($pdo, 'sales_order_items', 'notes') ? 'i.notes' : 'NULL AS notes'),
            'p.sku',
            'p.name AS product_name',
            'p.uom'
        ];

        $joinPackaging = '';
        if (table_exists($pdo, 'product_packaging_options') && column_exists($pdo, 'sales_order_items', 'packaging_option_id')) {
            $itemSelect[] = 'po.packaging_type';
            $itemSelect[] = 'po.packaging_name';
            $itemSelect[] = 'po.pieces_per_package';
            $itemSelect[] = 'po.packages_per_pallet';
            $joinPackaging = "
            LEFT JOIN product_packaging_options po
              ON po.id = i.packaging_option_id
             AND po.company_id = p.company_id
             AND po.product_id = i.product_id";
        } else {
            $itemSelect[] = "NULL AS packaging_type";
            $itemSelect[] = "NULL AS packaging_name";
            $itemSelect[] = "NULL AS pieces_per_package";
            $itemSelect[] = "NULL AS packages_per_pallet";
        }

        $sqlItems = "
            SELECT
                " . implode(",
                ", $itemSelect) . "
            FROM sales_order_items i
            JOIN products p
              ON p.id = i.product_id
            " . $joinPackaging . "
            WHERE i.order_id = ?
            ORDER BY p.sku
        ";
        $st = $pdo->prepare($sqlItems);
        $st->execute([$orderId]);
        $items = $st->fetchAll(PDO::FETCH_ASSOC);

        $forklift = botella_latest_forklift_order($pdo, $companyId, $locationId, $orderId);
        $truckPlan = botella_truck_plan_for_order($pdo, $companyId, $locationId, $orderId);

        $attachments = [];
        if (table_exists($pdo, 'sales_order_attachments')) {
            /*
             * Compatibilidad con versiones previas:
             * - En algunos dumps la ruta del archivo se llama relative_path.
             * - La fecha se llama uploaded_at en lugar de created_at.
             * El frontend espera file_path y created_at, por eso se normalizan aquí.
             */
            $attFileExpr = column_exists($pdo, 'sales_order_attachments', 'file_path')
                ? 'file_path'
                : (column_exists($pdo, 'sales_order_attachments', 'relative_path') ? 'relative_path AS file_path' : "'' AS file_path");
            $attDateExpr = column_exists($pdo, 'sales_order_attachments', 'created_at')
                ? 'created_at'
                : (column_exists($pdo, 'sales_order_attachments', 'uploaded_at') ? 'uploaded_at AS created_at' : 'NULL AS created_at');
            $attPurchaseExpr = column_exists($pdo, 'sales_order_attachments', 'purchase_order') ? 'purchase_order' : 'NULL AS purchase_order';
            $attMimeExpr = column_exists($pdo, 'sales_order_attachments', 'mime_type') ? 'mime_type' : 'NULL AS mime_type';
            $attSizeExpr = column_exists($pdo, 'sales_order_attachments', 'file_size') ? 'file_size' : 'NULL AS file_size';

            $st = $pdo->prepare("
                SELECT id, {$attPurchaseExpr}, original_name, stored_name, {$attFileExpr}, {$attMimeExpr}, {$attSizeExpr}, {$attDateExpr}
                FROM sales_order_attachments
                WHERE company_id=? AND location_id=? AND order_id=?
                ORDER BY id DESC
            ");
            $st->execute([$companyId, $locationId, $orderId]);
            $attachments = $st->fetchAll(PDO::FETCH_ASSOC);
        }

        $invoices = [];
        if (table_exists($pdo, 'sales_order_invoices')) {
            $st = $pdo->prepare("
                SELECT id, invoice_number, original_name, stored_name, file_path, mime_type, file_size, created_at
                FROM sales_order_invoices
                WHERE company_id=? AND location_id=? AND order_id=?
                ORDER BY id DESC
            ");
            $st->execute([$companyId, $locationId, $orderId]);
            $invoices = $st->fetchAll(PDO::FETCH_ASSOC);
        }

        jexit([
            'ok' => true,
            'order' => $order,
            'items' => $items,
            'forklift_order' => $forklift,
            'truck_plan' => $truckPlan,
            'attachments' => $attachments,
            'invoices' => $invoices
        ]);
    }



    if ($action === 'botella_invoice_upload') {
        if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso para cargar factura.'], 403);
        $orderId = (int)($_POST['order_id'] ?? 0);
        $invoiceNumber = trim((string)($_POST['invoice_number'] ?? ''));
        if ($orderId <= 0) jexit(['ok'=>false,'error'=>'order_id requerido.'], 400);
        if ($invoiceNumber === '') jexit(['ok'=>false,'error'=>'Captura el número de factura.'], 400);
        if (!isset($_FILES['invoice_file'])) jexit(['ok'=>false,'error'=>'Selecciona el archivo de factura.'], 400);

        $file = $_FILES['invoice_file'];
        if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) jexit(['ok'=>false,'error'=>'Selecciona el archivo de factura.'], 400);
        if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) jexit(['ok'=>false,'error'=>'No se pudo recibir el archivo de factura.'], 400);
        $size = (int)($file['size'] ?? 0);
        if ($size <= 0) jexit(['ok'=>false,'error'=>'El archivo de factura viene vacío.'], 400);
        if ($size > 10 * 1024 * 1024) jexit(['ok'=>false,'error'=>'El archivo de factura no debe exceder 10 MB.'], 400);
        $originalName = (string)($file['name'] ?? 'factura');
        $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
        if (!in_array($ext, botella_allowed_upload_ext(), true)) {
            jexit(['ok'=>false,'error'=>'Formato no permitido. Usa PDF, XML, JPG, JPEG, PNG o WEBP.'], 400);
        }

        $root = __DIR__ . '/uploads/ordenes_clientes';
        if (!is_dir($root)) {
            jexit(['ok'=>false,'error'=>'No existe la carpeta /uploads/ordenes_clientes para guardar la factura.'], 400);
        }
        if (!is_writable($root)) {
            jexit(['ok'=>false,'error'=>'La carpeta /uploads/ordenes_clientes no tiene permiso de escritura para PHP/Apache.'], 400);
        }

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("SELECT * FROM sales_orders WHERE id=? AND company_id=? AND location_id=? FOR UPDATE");
            $st->execute([$orderId, $companyId, $locationId]);
            $order = $st->fetch(PDO::FETCH_ASSOC);
            if (!$order) throw new RuntimeException('Orden no encontrada.');
            if (empty($order['outbound_id'])) throw new RuntimeException('Primero debe generarse la salida de almacén antes de cargar factura.');
            if (strtoupper((string)($order['status'] ?? '')) === 'ENTREGADA') throw new RuntimeException('La orden ya está marcada como entregada.');

            $safeInvoice = botella_safe_upload_name($invoiceNumber);
            $storedName = 'FACTURA_' . $safeInvoice . '_ORD_' . $orderId . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(3)) . '.' . $ext;
            $dest = $root . '/' . $storedName;
            if (!move_uploaded_file((string)$file['tmp_name'], $dest)) {
                throw new RuntimeException('No se pudo guardar el archivo de factura en el servidor.');
            }
            @chmod($dest, 0664);
            $relative = 'uploads/ordenes_clientes/' . $storedName;

            $st = $pdo->prepare("INSERT INTO sales_order_invoices(company_id, location_id, order_id, outbound_id, invoice_number, original_name, stored_name, file_path, mime_type, file_size, uploaded_by, created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,NOW())");
            $st->execute([
                $companyId,
                $locationId,
                $orderId,
                (int)($order['outbound_id'] ?? 0),
                mb_substr($invoiceNumber, 0, 80, 'UTF-8'),
                mb_substr($originalName, 0, 180, 'UTF-8'),
                $storedName,
                $relative,
                mb_substr((string)($file['type'] ?? ''), 0, 120, 'UTF-8'),
                $size,
                (int)$u['id']
            ]);
            $invoiceId = (int)$pdo->lastInsertId();

            $oldStatus = (string)($order['status'] ?? '');
            $st = $pdo->prepare("UPDATE sales_orders SET status='ENTREGADA', updated_at=NOW(), fulfilled_at=COALESCE(fulfilled_at, NOW()) WHERE id=? AND company_id=? AND location_id=?");
            $st->execute([$orderId, $companyId, $locationId]);

            if (table_exists($pdo, 'sales_order_status_log') && column_exists($pdo, 'sales_order_status_log', 'changed_by_user_id')) {
                $st = $pdo->prepare("INSERT INTO sales_order_status_log(order_id, old_status, new_status, changed_by_user_id, changed_by_role, comment, created_at) VALUES(?, ?, 'ENTREGADA', ?, 'ALMACEN', ?, NOW())");
                $st->execute([$orderId, $oldStatus, (int)$u['id'], 'Factura cargada: ' . $invoiceNumber]);
            }

            $pdo->commit();
            jexit(['ok'=>true, 'invoice_id'=>$invoiceId, 'invoice_number'=>$invoiceNumber, 'file_path'=>$relative, 'message'=>'Factura cargada. La orden cambió a ENTREGADA.']);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok'=>false,'error'=>$e->getMessage()], 400);
        }
    }

    if ($action === 'sales_dispatch_pdf') {
        $orderId = (int)($_GET['order_id'] ?? ($_GET['id'] ?? 0));
        $folioParam = trim((string)($_GET['folio'] ?? ($_GET['reference'] ?? '')));
        if ($orderId <= 0 && $folioParam === '') {
            http_response_code(400);
            echo 'order_id o folio requerido';
            exit;
        }

        $fpdfPath = __DIR__ . '/lib/fpdf.php';
        if (!is_file($fpdfPath)) {
            http_response_code(500);
            echo 'No se encontró la librería FPDF en: ' . $fpdfPath;
            exit;
        }
        require_once $fpdfPath;
        if (!class_exists('FPDF')) {
            http_response_code(500);
            echo 'La clase FPDF no está disponible.';
            exit;
        }

        $order = null;
        $items = [];
        $outbound = [];
        $pdfMode = '';

        // 1) Intentar con BReport sales_orders por ID exacto
        if ($orderId > 0) {
            $st = $pdo->prepare("
                SELECT
                    o.id,
                    o.sales_order_ref,
                    o.requested_delivery_date,
                    o.requested_delivery_time,
                    o.delivery_address,
                    o.comments,
                    o.status,
                    o.outbound_id,
                    o.fulfilled_at,
                    c.business_name,
                    c.contact_name,
                    c.phone
                FROM sales_orders o
                JOIN sales_customers c
                  ON c.id = o.customer_id
                WHERE o.id = ?
                  AND o.company_id = ?
                  AND o.location_id = ?
                LIMIT 1
            ");
            $st->execute([$orderId, $companyId, $locationId]);
            $order = $st->fetch(PDO::FETCH_ASSOC) ?: null;
            if ($order) {
                $pdfMode = 'BREPORT';
            }
        }

        // 2) Si no se encontró en BReport, buscar en VentasHielo por id / sales_order / ov_system / PW-id
        if (!$order) {
            $lookup = $folioParam;
            if ($lookup === '' && $orderId > 0) {
                $lookup = 'PW-' . $orderId;
            }

            if ($orderId > 0 || $lookup !== '') {
                $vhPdo = ventashielo_pdo_optional();
                if (!$vhPdo) {
                    http_response_code(500);
                    echo 'No se encontró conexión PDO a VentasHielo. Valida includes/db_ventashielo.php.';
                    exit;
                }

                $st = $vhPdo->prepare("
                    SELECT
                        o.id,
                        o.sales_order,
                        o.ov_system,
                        o.delivery_date,
                        o.delivery_time,
                        o.delivery_instructions,
                        o.comments,
                        o.status,
                        o.delivery_address,
                        o.breport_sales_order_id,
                        u.name  AS cliente,
                        u.phone AS cliente_phone,
                        u.address AS cliente_address
                    FROM orders o
                    JOIN users u
                      ON u.id = o.customer_id
                    WHERE o.id = ?
                       OR o.sales_order = ?
                       OR o.ov_system = ?
                       OR CONCAT('PW-', o.id) = ?
                    ORDER BY o.id DESC
                    LIMIT 1
                ");
                $idLookup = $orderId > 0 ? $orderId : 0;
                $st->execute([$idLookup, $lookup, $lookup, $lookup]);
                $order = $st->fetch(PDO::FETCH_ASSOC) ?: null;
                if ($order) {
                    $pdfMode = 'VH';
                }
            }
        }

        if (!$order) {
            http_response_code(404);
            echo 'Pedido no encontrado.';
            exit;
        }

        if ($pdfMode === 'BREPORT') {
            $st = $pdo->prepare("
                SELECT
                    i.qty,
                    p.sku,
                    p.name AS product_name,
                    p.uom
                FROM sales_order_items i
                JOIN products p
                  ON p.id = i.product_id
                WHERE i.order_id = ?
                ORDER BY p.sku
            ");
            $st->execute([(int)$order['id']]);
            $items = $st->fetchAll(PDO::FETCH_ASSOC);

            if (!empty($order['outbound_id'])) {
                $st = $pdo->prepare("
                    SELECT
                        ob.id AS outbound_id,
                        ob.outbound_date,
                        ob.reference,
                        ob.notes,
                        oc.name AS channel_name
                    FROM outbounds ob
                    LEFT JOIN outbound_channels oc
                      ON oc.id = ob.channel_id
                    WHERE ob.id = ?
                      AND ob.company_id = ?
                      AND ob.location_id = ?
                    LIMIT 1
                ");
                $st->execute([(int)$order['outbound_id'], $companyId, $locationId]);
                $outbound = $st->fetch(PDO::FETCH_ASSOC) ?: [];
            }
        } else {
            $vhPdo = ventashielo_pdo_optional();
            if (!$vhPdo) {
                http_response_code(500);
                echo 'No se encontrÃ³ conexiÃ³n PDO a VentasHielo. Valida includes/db_ventashielo.php.';
                exit;
            }

            $st = $vhPdo->prepare("
                SELECT
                    oi.quantity,
                    p.id AS product_id,
                    p.name AS product_name
                FROM order_items oi
                JOIN products p
                  ON p.id = oi.product_id
                WHERE oi.order_id = ?
                ORDER BY p.name
            ");
            $st->execute([(int)$order['id']]);
            $items = $st->fetchAll(PDO::FETCH_ASSOC);
        }

        @ini_set('display_errors', '0');
        @ini_set('display_startup_errors', '0');

        $pdf = new FPDF('P', 'mm', 'Letter');
        $pdf->AddPage();

        $logoPath = __DIR__ . '/assets/blanquita.png';
        if (is_file($logoPath)) {
            $pdf->Image($logoPath, 10, 8, 50);
        }

        $pdf->SetY(22);
        $pdf->SetFont('Arial','B',14);
        $pdf->Cell(0, 8, pdf_text('Orden de entrega'), 0, 1, 'C');
        $pdf->Ln(2);

        if ($pdfMode === 'BREPORT') {
            $folio = (string)($order['sales_order_ref'] ?: ('PED-' . $order['id']));
            $cliente = trim((string)($order['business_name'] ?? ''));
            $contacto = trim((string)($order['contact_name'] ?? ''));
            $telefono = trim((string)($order['phone'] ?? ''));

            $pdf->SetFont('Arial','',11);
            $pdf->Cell(0, 6, pdf_text('Pedido #' . $order['id']), 0, 1);
            $pdf->Cell(0, 6, pdf_text('Folio ventas: ' . $folio), 0, 1);
            $pdf->Cell(0, 6, pdf_text('Cliente: ' . ($cliente !== '' ? $cliente : '-')), 0, 1);

            if ($contacto !== '' || $telefono !== '') {
                $pdf->Cell(0, 6, pdf_text('Contacto: ' . $contacto . ($telefono !== '' ? ' (' . $telefono . ')' : '')), 0, 1);
            }

            $pdf->Cell(0, 6, pdf_text('Dirección: ' . ((string)($order['delivery_address'] ?? '') ?: '-')), 0, 1);
            $pdf->Cell(0, 6, pdf_text('Fecha solicitada: ' . ((string)($order['requested_delivery_date'] ?? '') ?: '-')), 0, 1);
            if (!empty($order['requested_delivery_time'])) {
                $pdf->Cell(0, 6, pdf_text('Hora solicitada: ' . $order['requested_delivery_time']), 0, 1);
            }
            if (!empty($outbound['outbound_date'])) {
                $pdf->Cell(0, 6, pdf_text('Fecha surtido: ' . $outbound['outbound_date']), 0, 1);
            }
            if (!empty($outbound['reference'])) {
                $pdf->Cell(0, 6, pdf_text('Referencia almacén: ' . $outbound['reference']), 0, 1);
            }
            if (!empty($outbound['channel_name'])) {
                $pdf->Cell(0, 6, pdf_text('Canal: ' . $outbound['channel_name']), 0, 1);
            }
            $pdf->Ln(4);
            if (!empty($order['comments'])) {
                $pdf->SetFont('Arial','B',11);
                $pdf->Cell(0, 6, pdf_text('Comentarios del pedido:'), 0, 1);
                $pdf->SetFont('Arial','',11);
                $pdf->MultiCell(0, 6, pdf_text((string)$order['comments']));
                $pdf->Ln(2);
            }
            if (!empty($outbound['notes'])) {
                $pdf->SetFont('Arial','B',11);
                $pdf->Cell(0, 6, pdf_text('Notas de salida:'), 0, 1);
                $pdf->SetFont('Arial','',11);
                $pdf->MultiCell(0, 6, pdf_text((string)$outbound['notes']));
                $pdf->Ln(2);
            }

            $pdf->SetFont('Arial','B',11);
            $pdf->Cell(28, 7, pdf_text('Cant.'), 1, 0, 'C');
            $pdf->Cell(42, 7, pdf_text('SKU'), 1, 0, 'C');
            $pdf->Cell(0, 7, pdf_text('Producto'), 1, 1, 'C');
            $pdf->SetFont('Arial','',11);
            if (!$items) {
                $pdf->Cell(28, 7, pdf_text('-'), 1, 0, 'C');
                $pdf->Cell(42, 7, pdf_text('-'), 1, 0, 'C');
                $pdf->Cell(0, 7, pdf_text('Sin artículos registrados'), 1, 1);
            } else {
                foreach ($items as $it) {
                    $qtyValue = isset($it['qty']) ? (float)$it['qty'] : (float)($it['quantity'] ?? 0);
                    $sku = (string)($it['sku'] ?? '');
                    $name = (string)($it['product_name'] ?? '');
                    $pdf->Cell(28, 7, pdf_text(number_format($qtyValue, 3, '.', '')), 1, 0, 'C');
                    $pdf->Cell(42, 7, pdf_text($sku), 1, 0, 'L');
                    $pdf->Cell(0, 7, pdf_text($name), 1, 1, 'L');
                }
            }
        } else {
            $folio = normalize_sales_folio($order, 'PW');
            $cliente = trim((string)($order['cliente'] ?? ''));
            $telefono = trim((string)($order['cliente_phone'] ?? ''));

            $pdf->SetFont('Arial', '', 11);
            $pdf->Cell(0, 6, pdf_text('Pedido #' . $order['id']), 0, 1);
            $pdf->Cell(0, 6, pdf_text('Orden de compra / Folio: ' . $folio), 0, 1);
            $pdf->Cell(0, 6, pdf_text('Cliente: ' . ($cliente !== '' ? $cliente : '-')), 0, 1);
            if ($telefono !== '') {
                $pdf->Cell(0, 6, pdf_text('Teléfono: ' . $telefono), 0, 1);
            }
            $direccionEntrega = (string)($order['delivery_address'] ?? '');
            if ($direccionEntrega === '') {
                $direccionEntrega = (string)($order['cliente_address'] ?? '');
            }
            $pdf->Cell(0, 6, pdf_text('Dirección: ' . ($direccionEntrega !== '' ? $direccionEntrega : '-')), 0, 1);
            $pdf->Cell(0, 6, pdf_text('Fecha de entrega: ' . ((string)($order['delivery_date'] ?? '') ?: '-')), 0, 1);
            if (!empty($order['delivery_time'])) {
                $pdf->Cell(0, 6, pdf_text('Hora de entrega: ' . (string)$order['delivery_time']), 0, 1);
            }
            $pdf->Ln(4);
            if (!empty($order['delivery_instructions'])) {
                $pdf->SetFont('Arial', 'B', 11);
                $pdf->Cell(0, 6, pdf_text('Instrucciones de entrega:'), 0, 1);
                $pdf->SetFont('Arial', '', 11);
                $pdf->MultiCell(0, 6, pdf_text((string)$order['delivery_instructions']));
                $pdf->Ln(2);
            }
            if (!empty($order['comments'])) {
                $pdf->SetFont('Arial', 'B', 11);
                $pdf->Cell(0, 6, pdf_text('Comentarios del pedido:'), 0, 1);
                $pdf->SetFont('Arial', '', 11);
                $pdf->MultiCell(0, 6, pdf_text((string)$order['comments']));
                $pdf->Ln(2);
            }
            $pdf->SetFont('Arial', 'B', 11);
            $pdf->Cell(35, 7, pdf_text('Cantidad'), 1, 0, 'C');
            $pdf->Cell(0, 7, pdf_text('Producto'), 1, 1, 'C');
            $pdf->SetFont('Arial', '', 11);
            if (!$items) {
                $pdf->Cell(35, 7, pdf_text('-'), 1, 0, 'C');
                $pdf->Cell(0, 7, pdf_text('Sin artículos registrados'), 1, 1);
            } else {
                foreach ($items as $it) {
                    $cantidad = number_format((float)($it['quantity'] ?? 0), 3, '.', '');
                    $pdf->Cell(35, 7, pdf_text($cantidad), 1, 0, 'C');
                    $pdf->Cell(0, 7, pdf_text((string)$it['product_name']), 1, 1);
                }
            }
        }

        $pdf->Ln(10);
        $pdf->Cell(0, 6, pdf_text('Nombre y Firma de quien recibe: ___________________________'), 0, 1);
        $pdf->Ln(8);
        $pdf->Cell(0, 6, pdf_text('Nombre y Firma de quien entrega: ___________________________ Fecha: ___/___/_____'), 0, 1);

        $filename = safe_pdf_filename('orden_entrega_' . $folio) . '.pdf';
        $pdf->Output('D', $filename);
        exit;
    }

    if ($action === 'sales_dispatch') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $d = read_json();
        $orderId    = (int)arr_get($d, 'order_id', 0);
        $workDate   = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate = breport_resolve_work_date($workDate);
        $channelId  = (int)arr_get($d, 'channel_id', 0);
        $reference  = trim((string)arr_get($d, 'reference', ''));
        $notes      = trim((string)arr_get($d, 'notes', ''));
        $driverName = trim((string)arr_get($d, 'driver_name', ''));
        $vehicle    = trim((string)arr_get($d, 'vehicle_unit', ''));
        $departAt   = trim((string)arr_get($d, 'depart_at', ''));

        if ($orderId <= 0) {
            jexit(['ok' => false, 'error' => 'order_id requerido.'], 400);
        }
        if ($channelId <= 0) {
            jexit(['ok' => false, 'error' => 'Canal requerido.'], 400);
        }

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("
                SELECT
                    o.*,
                    c.business_name,
                    c.contact_name,
                    c.phone
                FROM sales_orders o
                JOIN sales_customers c
                  ON c.id = o.customer_id
                WHERE o.id = ?
                  AND o.company_id = ?
                  AND o.location_id = ?
                FOR UPDATE
            ");
            $st->execute([$orderId, $companyId, $locationId]);
            $order = $st->fetch(PDO::FETCH_ASSOC);

            if (!$order) {
                throw new RuntimeException('Pedido no encontrado.');
            }
            if ((string)$order['status'] !== 'AUTORIZADA') {
                throw new RuntimeException('Solo se pueden surtir pedidos AUTORIZADA.');
            }
            if (!empty($order['outbound_id'])) {
                throw new RuntimeException('Este pedido ya fue ligado a una salida de almacén.');
            }

            $st = $pdo->prepare("
                SELECT
                    i.product_id,
                    i.packaging_option_id,
                    i.qty,
                    i.packages_qty,
                    i.pallets_qty,
                    i.pallet_type,
                    i.pallet_specification,
                    p.sku,
                    p.name,
                    p.uom,
                    po.packaging_name,
                    po.pieces_per_package,
                    po.packages_per_pallet
                FROM sales_order_items i
                JOIN products p
                  ON p.id = i.product_id
                 AND p.company_id = ?
                LEFT JOIN product_packaging_options po
                  ON po.id = i.packaging_option_id
                 AND po.company_id = ?
                 AND po.product_id = i.product_id
                 AND po.active = 1
                WHERE i.order_id = ?
                ORDER BY p.sku DESC
            ");
            $st->execute([$companyId, $companyId, $orderId]);
            $items = $st->fetchAll(PDO::FETCH_ASSOC);

            if (!$items) {
                throw new RuntimeException('El pedido no tiene artículos.');
            }

            foreach ($items as $it) {
                $sku = (string)($it['sku'] ?? '');
                $qty = (float)($it['qty'] ?? 0);
                $packagesQty = (int)($it['packages_qty'] ?? 0);
                $palletsQty = (int)($it['pallets_qty'] ?? 0);
                $piecesPerPackage = (int)($it['pieces_per_package'] ?? 0);
                $packagesPerPallet = (int)($it['packages_per_pallet'] ?? 0);

                if ((int)($it['packaging_option_id'] ?? 0) <= 0 || $piecesPerPackage <= 0 || $packagesPerPallet <= 0) {
                    throw new RuntimeException('[LOGISTICS] El artículo ' . $sku . ' no cuenta con un embalaje válido. Regresa a Ventas y selecciona el embalaje antes de surtir.');
                }
                if ($packagesQty <= 0) {
                    throw new RuntimeException('[LOGISTICS] El artículo ' . $sku . ' no tiene paquetes capturados.');
                }

                $calculatedPallets = $packagesQty / $packagesPerPallet;
                if (abs($calculatedPallets - round($calculatedPallets)) > 0.000001) {
                    throw new RuntimeException(
                        '[PALLET_DECIMAL] La salida de ' . $sku . ' contiene ' . $packagesQty .
                        ' paquetes y genera ' . number_format($calculatedPallets, 3, '.', '') .
                        ' tarimas. No se permiten tarimas decimales; solicita a Ventas corregir la orden.'
                    );
                }

                $expectedPallets = (int)round($calculatedPallets);
                $expectedBottles = $packagesQty * $piecesPerPackage;
                if ($palletsQty !== $expectedPallets || abs($qty - $expectedBottles) > 0.000001) {
                    throw new RuntimeException(
                        '[LOGISTICS] La orden de ' . $sku . ' no coincide: debe surtir ' .
                        $expectedPallets . ' tarimas completas y ' . $expectedBottles .
                        ' botellas. Regresa a Ventas para corregirla.'
                    );
                }
                if (abs($qty - round($qty)) > 0.000001) {
                    throw new RuntimeException('[LOGISTICS] Botella no permite salidas fraccionarias de inventario para ' . $sku . '.');
                }

                $available = get_product_outbound_available($pdo, $companyId, $locationId, (int)$it['product_id']);
                if ($qty > $available) {
                    throw new RuntimeException('Inventario insuficiente para el producto: ' . $sku . '. Requerido: ' . number_format($qty, 0) . ' botellas.');
                }
            }

            $forklift = botella_latest_forklift_order($pdo, $companyId, $locationId, $orderId);
            if (!$forklift || strtoupper((string)($forklift['status'] ?? '')) !== 'COMPLETA') {
                throw new RuntimeException('[FORKLIFT] No se puede generar la salida. Primero Almacén debe enviar la orden a montacargas y el montacarguista debe confirmar carga completa del camión.');
            }
            if ($vehicle === '') $vehicle = (string)($forklift['vehicle_unit'] ?? '');
            if ($driverName === '') $driverName = (string)($forklift['driver_name'] ?? '');

            $routeNoForLoad = (int)($order['route_no'] ?? 0);
            $routeInfoForLoad = botella_route_row($pdo, $companyId, $locationId, $routeNoForLoad);
            $routeWarehouseId = botella_ensure_route_warehouse($pdo, $companyId, $locationId, $routeNoForLoad);
            $routeWarehouseCode = botella_route_warehouse_code($routeNoForLoad);
            $routeLabelForLoad = trim((string)($routeInfoForLoad['route_code'] ?? '') . ' · ' . (string)($routeInfoForLoad['route_name'] ?? ''));
            if ($routeLabelForLoad === '·') $routeLabelForLoad = 'Ruta ' . $routeNoForLoad;

            $notesExtra = [];
            $notesExtra[] = 'Pedido ventas #' . $orderId;
            if (!empty($order['sales_order_ref'])) $notesExtra[] = 'Folio ventas: ' . $order['sales_order_ref'];
            if (!empty($order['business_name'])) $notesExtra[] = 'Cliente/Destino: ' . $order['business_name'];
            if ($vehicle !== '') $notesExtra[] = 'Transporte: ' . $vehicle;
            $notesExtra[] = 'Ruta destino inventario: ' . $routeWarehouseCode . ' ' . $routeLabelForLoad;
            if ($departAt !== '') $notesExtra[] = 'Salida: ' . $departAt;
            if (!empty($order['comments'])) $notesExtra[] = 'Notas Ventas: ' . trim((string)$order['comments']);
            if ($notes !== '') $notesExtra[] = 'Notas Almacén: ' . $notes;
            $notesExtra[] = 'Carga Botella a ruta móvil; inventario queda disponible en la ruta hasta su venta/devolución/resurtido';

            $fullNotes = implode(' | ', $notesExtra);
            if ($reference === '') {
                $reference = !empty($order['sales_order_ref']) ? (string)$order['sales_order_ref'] : ('PED-' . $orderId);
            }

            $st = $pdo->prepare("
                INSERT INTO outbounds (
                    company_id, location_id, outbound_date, channel_id, status,
                    created_by, reference, notes
                ) VALUES (
                    ?, ?, ?, ?, 'POSTED',
                    ?, ?, ?
                )
            ");
            $st->execute([
                $companyId,
                $locationId,
                $workDate,
                $channelId,
                (int)$u['id'],
                $reference,
                $fullNotes
            ]);
            $outboundId = (int)$pdo->lastInsertId();

            $stItem = $pdo->prepare("
                INSERT INTO outbound_items (
                    outbound_id, product_id, qty,
                    packaging_option_id, packages_qty, pallets_qty,
                    pallet_type, pallet_specification
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            ");

            $stMoveOut = $pdo->prepare("
                INSERT INTO inventory_moves (
                    company_id, location_id, warehouse_id, work_date, product_id, qty, move_type,
                    channel_id, ref_type, ref_id, notes, created_by, created_at
                ) VALUES (
                    ?, ?, ?, ?, ?, ?, 'OUT',
                    ?, 'ROUTE_LOAD_OUT', ?, ?, ?, NOW()
                )
            ");

            $stMoveInRoute = $pdo->prepare("
                INSERT INTO inventory_moves (
                    company_id, location_id, warehouse_id, work_date, product_id, qty, move_type,
                    channel_id, ref_type, ref_id, notes, created_by, created_at
                ) VALUES (
                    ?, ?, ?, ?, ?, ?, 'IN',
                    ?, 'ROUTE_LOAD_IN', ?, ?, ?, NOW()
                )
            ");

            /*
             * Botella puede traer el mismo artículo repetido en la carga
             * (por ejemplo misma SKU en diferentes renglones/acomodos).
             * La tabla outbound_items tiene llave única por salida + producto,
             * por eso se consolida por product_id antes de insertar.
             * Esto evita el error uk_out_item sin duplicar inventario.
             */
            $itemsByProduct = [];
            foreach ($items as $it) {
                $pid = (int)($it['product_id'] ?? 0);
                if ($pid <= 0) continue;

                if (!isset($itemsByProduct[$pid])) {
                    $itemsByProduct[$pid] = [
                        'product_id' => $pid,
                        'qty' => 0.0,
                        'packaging_option_id' => (int)($it['packaging_option_id'] ?? 0) ?: null,
                        'packages_qty' => 0,
                        'pallets_qty' => 0,
                        'pallet_type' => $it['pallet_type'] ?? null,
                        'pallet_specification' => $it['pallet_specification'] ?? null,
                    ];
                }

                $itemsByProduct[$pid]['qty'] += (float)($it['qty'] ?? 0);
                $itemsByProduct[$pid]['packages_qty'] += (int)($it['packages_qty'] ?? 0);
                $itemsByProduct[$pid]['pallets_qty'] += (int)($it['pallets_qty'] ?? 0);

                $pkgId = (int)($it['packaging_option_id'] ?? 0);
                if ($pkgId > 0 && $itemsByProduct[$pid]['packaging_option_id'] !== null && $itemsByProduct[$pid]['packaging_option_id'] !== $pkgId) {
                    $itemsByProduct[$pid]['packaging_option_id'] = null;
                }

                $palletType = $it['pallet_type'] ?? null;
                if ($palletType !== null && $itemsByProduct[$pid]['pallet_type'] !== null && $itemsByProduct[$pid]['pallet_type'] !== $palletType) {
                    $itemsByProduct[$pid]['pallet_type'] = 'Mixto';
                }

                $palletSpec = $it['pallet_specification'] ?? null;
                if ($palletSpec !== null && $itemsByProduct[$pid]['pallet_specification'] !== null && $itemsByProduct[$pid]['pallet_specification'] !== $palletSpec) {
                    $itemsByProduct[$pid]['pallet_specification'] = 'Mixto';
                }
            }

            foreach ($itemsByProduct as $it) {
                $productIdMove = (int)$it['product_id'];
                $qtyMove = (float)$it['qty'];
                if ($qtyMove <= 0) continue;

                $stItem->execute([
                    $outboundId,
                    $productIdMove,
                    $qtyMove,
                    $it['packaging_option_id'],
                    (int)($it['packages_qty'] ?? 0) ?: null,
                    (int)($it['pallets_qty'] ?? 0) ?: null,
                    ($it['pallet_type'] ?? null),
                    ($it['pallet_specification'] ?? null)
                ]);

                foreach (allocate_inventory_out($pdo, $companyId, $locationId, $productIdMove, $qtyMove) as $alloc) {
                    $qtyAllocated = (float)$alloc['qty'];

                    // 1) Sale del almacén físico de la planta.
                    $stMoveOut->execute([
                        $companyId,
                        $locationId,
                        $alloc['warehouse_id'],
                        $workDate,
                        $productIdMove,
                        $qtyAllocated,
                        $channelId,
                        $outboundId,
                        $fullNotes,
                        (int)$u['id']
                    ]);

                    // 2) Entra al almacén móvil de la ruta.
                    // El producto queda como inventario de ruta; si no se vende,
                    // será inventario inicial de esa ruta al día siguiente.
                    $stMoveInRoute->execute([
                        $companyId,
                        $locationId,
                        $routeWarehouseId,
                        $workDate,
                        $productIdMove,
                        $qtyAllocated,
                        $channelId,
                        $outboundId,
                        $fullNotes,
                        (int)$u['id']
                    ]);
                }
            }

            $st = $pdo->prepare("
                UPDATE sales_orders
                SET outbound_id = ?, status = 'SURTIENDO', fulfilled_at = NOW(), updated_at = NOW()
                WHERE id = ?
            ");
            $st->execute([$outboundId, $orderId]);

            if (column_exists($pdo, 'sales_order_status_log', 'changed_by_user_id')) {
                $st = $pdo->prepare("
                    INSERT INTO sales_order_status_log (
                        order_id, old_status, new_status, changed_by_user_id, changed_by_role, comment, created_at
                    ) VALUES (?, ?, ?, ?, ?, ?, NOW())
                ");
                $st->execute([
                    $orderId,
                    'AUTORIZADA',
                    'SURTIENDO',
                    (int)$u['id'],
                    'ALMACEN',
                    'Carga de ruta generada. Outbound #' . $outboundId . ' · Inventario enviado a ' . $routeWarehouseCode
                ]);
            }

            $pdo->commit();
            jexit([
                'ok' => true,
                'outbound_id' => $outboundId,
                'route_no' => $routeNoForLoad,
                'route_warehouse_id' => $routeWarehouseId,
                'route_warehouse_code' => $routeWarehouseCode,
                'message' => 'Carga enviada a inventario de ruta. La existencia restante en la ruta será inventario inicial del siguiente día operativo.',
                'pdf_url' => build_sales_dispatch_pdf_url($orderId, (string)$reference)
            ]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            $message = $e->getMessage();
            $validationCode = 'DISPATCH_ERROR';
            if (str_starts_with($message, '[PALLET_DECIMAL]')) {
                $validationCode = 'PALLET_DECIMAL';
                $message = str_replace('[PALLET_DECIMAL] ', '', $message);
            } elseif (str_starts_with($message, '[LOGISTICS]')) {
                $validationCode = 'LOGISTICS';
                $message = str_replace('[LOGISTICS] ', '', $message);
            } elseif (str_starts_with($message, '[FORKLIFT]')) {
                $validationCode = 'FORKLIFT';
                $message = str_replace('[FORKLIFT] ', '', $message);
            }
            jexit(['ok' => false, 'validation_code' => $validationCode, 'error' => $message], 400);
        }
    }

    if ($action === 'botella_manual_packaging_options') {
        $productId = (int)($_GET['product_id'] ?? 0);
        if ($productId <= 0) jexit(['ok'=>false, 'error'=>'Artículo requerido.'], 400);

        $rows = [];
        try {
            $st = $pdo->prepare("
                SELECT
                    po.id,
                    po.product_id,
                    po.packaging_type,
                    po.packaging_name,
                    po.pieces_per_package,
                    po.packages_per_pallet,
                    po.is_default,
                    0 AS legacy_fallback,
                    (po.pieces_per_package * po.packages_per_pallet) AS pieces_per_pallet
                FROM product_packaging_options po
                JOIN products p ON p.id = po.product_id AND p.company_id = po.company_id
                WHERE po.company_id = ?
                  AND po.product_id = ?
                  AND po.active = 1
                  AND p.active = 1
                ORDER BY po.is_default DESC, po.packaging_type, po.packages_per_pallet DESC
            ");
            $st->execute([$companyId, $productId]);
            $rows = $st->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable $e) {
            $rows = [];
        }

        if (!$rows) {
            $st = $pdo->prepare("
                SELECT
                    id AS product_id,
                    sku,
                    name,
                    pieces_per_package,
                    packages_per_pallet
                FROM products
                WHERE id = ?
                  AND company_id = ?
                  AND active = 1
                LIMIT 1
            ");
            $st->execute([$productId, $companyId]);
            $p = $st->fetch(PDO::FETCH_ASSOC);
            if ($p && (int)($p['pieces_per_package'] ?? 0) > 0 && (int)($p['packages_per_pallet'] ?? 0) > 0) {
                $rows[] = [
                    'id' => 0,
                    'product_id' => (int)$p['product_id'],
                    'packaging_type' => 'TARIMA',
                    'packaging_name' => 'Embalaje principal · ' . ((int)$p['packages_per_pallet']) . ' paq/tar',
                    'pieces_per_package' => (int)$p['pieces_per_package'],
                    'packages_per_pallet' => (int)$p['packages_per_pallet'],
                    'is_default' => 1,
                    'legacy_fallback' => 1,
                    'pieces_per_pallet' => ((int)$p['pieces_per_package'] * (int)$p['packages_per_pallet']),
                ];
            }
        }
        jexit(['ok'=>true,'rows'=>$rows]);
    }


    if ($action === 'botella_manual_routes') {
        /*
         * Almacén Botella · OSA para reparto:
         * No se debe mostrar la ruta especial de VENTAS/CARGA DE CLIENTES.
         * Esa ruta no es reparto operativo; solo rutas reales de reparto pueden recibir carga.
         */
        $st = $pdo->prepare("
            SELECT route_no, route_code, route_name, driver_name, vehicle_unit, delivery_days
            FROM route_access_codes
            WHERE company_id = ?
              AND location_id = ?
              AND active = 1
              AND route_no <> 1
              AND UPPER(COALESCE(route_code,'')) NOT LIKE '%VENTA%'
              AND UPPER(COALESCE(route_name,'')) NOT LIKE '%VENTA%'
              AND UPPER(COALESCE(route_name,'')) NOT LIKE '%CARGA DE CLIENTES%'
            ORDER BY sort_order, route_no, route_name
        ");
        $st->execute([$companyId, $locationId]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'botella_route_current_stock') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $routeNo = (int)($_GET['route_no'] ?? 0);
        if ($routeNo <= 0) {
            jexit(['ok' => false, 'error' => 'Ruta requerida.'], 400);
        }

        try {
            $route = botella_route_row($pdo, $companyId, $locationId, $routeNo);
            $routeCodeText = strtoupper(trim((string)($route['route_code'] ?? '')));
            $routeNameText = strtoupper(trim((string)($route['route_name'] ?? '')));
            if ($routeNo === 1 || str_contains($routeCodeText, 'VENTA') || str_contains($routeNameText, 'VENTA') || str_contains($routeNameText, 'CARGA DE CLIENTES')) {
                throw new RuntimeException('La ruta seleccionada es ruta de ventas/carga de clientes y no puede usarse como ruta de reparto.');
            }

            $routeWarehouseId = botella_ensure_route_warehouse($pdo, $companyId, $locationId, $routeNo);
            $workDate = breport_resolve_work_date((string)($_GET['work_date'] ?? breport_operational_today()));
            $win = breport_operational_window($workDate);

            $st = $pdo->prepare("
                SELECT
                    p.id AS product_id,
                    p.sku,
                    p.name,
                    COALESCE(ppo.packaging_name, CONCAT(COALESCE(p.packages_per_pallet,0),' paquetes por tarima')) AS packaging_name,
                    COALESCE(ppo.pieces_per_package, p.pieces_per_package, 0) AS pieces_per_package,
                    COALESCE(ppo.packages_per_pallet, p.packages_per_pallet, 0) AS packages_per_pallet,
                    COALESCE(SUM(CASE WHEN im.move_type IN ('IN','ADJ') THEN im.qty WHEN im.move_type='OUT' THEN -im.qty ELSE 0 END),0) AS current_qty,
                    COALESCE(SUM(CASE WHEN im.move_type IN ('IN','ADJ') AND COALESCE(im.created_at, CONCAT(im.work_date,' 12:00:00')) >= ? AND COALESCE(im.created_at, CONCAT(im.work_date,' 12:00:00')) < ? THEN im.qty ELSE 0 END),0) AS in_qty,
                    COALESCE(SUM(CASE WHEN im.move_type='OUT' AND COALESCE(im.created_at, CONCAT(im.work_date,' 12:00:00')) >= ? AND COALESCE(im.created_at, CONCAT(im.work_date,' 12:00:00')) < ? THEN im.qty ELSE 0 END),0) AS out_qty
                FROM products p
                LEFT JOIN product_packaging_options ppo ON ppo.company_id=p.company_id AND ppo.product_id=p.id AND ppo.active=1 AND ppo.is_default=1
                LEFT JOIN inventory_moves im ON im.company_id=p.company_id AND im.location_id=? AND im.product_id=p.id AND im.warehouse_id=?
                WHERE p.company_id=? AND p.active=1
                GROUP BY p.id, p.sku, p.name, ppo.packaging_name, ppo.pieces_per_package, ppo.packages_per_pallet, p.pieces_per_package, p.packages_per_pallet
                HAVING current_qty <> 0 OR in_qty <> 0 OR out_qty <> 0
                ORDER BY p.sku, p.name
            ");
            $st->execute([$win['start'], $win['end'], $win['start'], $win['end'], $locationId, $routeWarehouseId, $companyId]);

            $rows = [];
            $totals = ['pieces' => 0.0, 'packages' => 0.0, 'pallets' => 0.0, 'products' => 0];
            foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
                $qty = (float)($r['current_qty'] ?? 0);
                $ppp = (int)($r['pieces_per_package'] ?? 0);
                $ppt = (int)($r['packages_per_pallet'] ?? 0);
                $packages = $ppp > 0 ? ($qty / $ppp) : 0;
                $pallets = ($ppp > 0 && $ppt > 0) ? ($qty / ($ppp * $ppt)) : 0;
                $rows[] = [
                    'product_id' => (int)$r['product_id'],
                    'sku' => (string)$r['sku'],
                    'name' => (string)$r['name'],
                    'packaging_name' => (string)($r['packaging_name'] ?? ''),
                    'pieces_per_package' => $ppp,
                    'packages_per_pallet' => $ppt,
                    'current_qty' => round($qty, 3),
                    'packages' => round($packages, 3),
                    'pallets' => round($pallets, 3),
                    'in_qty' => round((float)($r['in_qty'] ?? 0), 3),
                    'out_qty' => round((float)($r['out_qty'] ?? 0), 3),
                ];
                $totals['pieces'] += $qty;
                $totals['packages'] += $packages;
                $totals['pallets'] += $pallets;
            }
            $totals['products'] = count($rows);

            jexit([
                'ok' => true,
                'route' => $route,
                'warehouse_id' => $routeWarehouseId,
                'warehouse_code' => botella_route_warehouse_code($routeNo),
                'work_date' => $workDate,
                'rows' => $rows,
                'totals' => [
                    'pieces' => round((float)$totals['pieces'], 3),
                    'packages' => round((float)$totals['packages'], 3),
                    'pallets' => round((float)$totals['pallets'], 3),
                    'products' => (int)$totals['products'],
                ],
            ]);
        } catch (Throwable $e) {
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }
    }

    if ($action === 'botella_manual_customers') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $q = trim((string)($_GET['q'] ?? ''));
        $hasLocation = column_exists($pdo, 'sales_customers', 'location_id');

        $where = ["c.company_id = ?", "c.active = 1"];
        $params = [$companyId];

        if ($hasLocation) {
            $where[] = "(c.location_id IS NULL OR c.location_id = ?)";
            $params[] = $locationId;
        }

        if ($q !== '') {
            $where[] = "(c.customer_code LIKE ? OR c.business_name LIKE ? OR c.contact_name LIKE ? OR c.address LIKE ?)";
            for ($i = 0; $i < 4; $i++) $params[] = '%' . $q . '%';
        }

        $locSelect = $hasLocation ? "c.location_id," : "NULL AS location_id,";
        $st = $pdo->prepare("
            SELECT
                c.id,
                {$locSelect}
                c.customer_code,
                c.business_name,
                c.contact_name,
                c.phone,
                c.email,
                c.address,
                c.postal_code,
                c.active,
                l.name AS location_name,
                l.code AS location_code,
                (
                    SELECT COUNT(*)
                    FROM sales_customer_addresses a
                    WHERE a.customer_id = c.id
                      AND a.active = 1
                ) AS addresses_count
            FROM sales_customers c
            LEFT JOIN locations l ON l.id = c.location_id AND l.company_id = c.company_id
            WHERE " . implode(' AND ', $where) . "
            ORDER BY c.business_name ASC, c.customer_code ASC
            LIMIT 500
        ");
        $st->execute($params);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'botella_manual_customer_addresses') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $customerId = (int)($_GET['customer_id'] ?? 0);
        if ($customerId <= 0) {
            jexit(['ok' => false, 'error' => 'Cliente requerido.'], 400);
        }

        $hasCustomerLocation = column_exists($pdo, 'sales_customers', 'location_id');
        $hasAddrLocation = column_exists($pdo, 'sales_customer_addresses', 'location_id');

        $sqlCustomer = "
            SELECT id, customer_code, business_name, contact_name, address, postal_code,
                   " . ($hasCustomerLocation ? "location_id" : "NULL AS location_id") . "
            FROM sales_customers
            WHERE id = ?
              AND company_id = ?
              AND active = 1
        ";
        $paramsCustomer = [$customerId, $companyId];
        if ($hasCustomerLocation) {
            $sqlCustomer .= " AND (location_id IS NULL OR location_id = ?)";
            $paramsCustomer[] = $locationId;
        }
        $sqlCustomer .= " LIMIT 1";
        $stCustomer = $pdo->prepare($sqlCustomer);
        $stCustomer->execute($paramsCustomer);
        $customer = $stCustomer->fetch(PDO::FETCH_ASSOC);
        if (!$customer) {
            jexit(['ok' => false, 'error' => 'Cliente inválido para esta localidad.'], 404);
        }

        $locSelect = $hasAddrLocation ? "a.location_id," : "NULL AS location_id,";
        $where = ["a.customer_id = ?", "a.active = 1"];
        $params = [$customerId];
        if ($hasAddrLocation) {
            $where[] = "(a.location_id IS NULL OR a.location_id = ?)";
            $params[] = $locationId;
        }

        $st = $pdo->prepare("
            SELECT
                a.id,
                a.customer_id,
                {$locSelect}
                a.label,
                a.address,
                a.postal_code,
                a.is_default,
                a.active,
                l.name AS location_name,
                l.code AS location_code
            FROM sales_customer_addresses a
            LEFT JOIN locations l ON l.id = a.location_id
            WHERE " . implode(' AND ', $where) . "
            ORDER BY a.is_default DESC, a.id DESC
        ");
        $st->execute($params);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);

        if (!$rows && trim((string)($customer['address'] ?? '')) !== '') {
            $rows[] = [
                'id' => 0,
                'customer_id' => $customerId,
                'location_id' => $customer['location_id'] ?? null,
                'label' => 'Punto de entrega principal',
                'address' => (string)$customer['address'],
                'postal_code' => (string)($customer['postal_code'] ?? ''),
                'is_default' => 1,
                'active' => 1,
                'location_name' => $GLOBALS['scopeLocationName'] ?? '',
                'location_code' => '',
            ];
        }

        jexit(['ok' => true, 'customer' => $customer, 'rows' => $rows]);
    }


    if ($action === 'out_save') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $d = read_json();
        $orderType = strtoupper(trim((string)arr_get($d, 'order_type', 'RUTA')));
        if (!in_array($orderType, ['CLIENTE','RUTA'], true)) $orderType = 'RUTA';

        $workDate = breport_resolve_work_date(trim((string)arr_get($d, 'work_date', breport_operational_today())));
        $channelId = (int)arr_get($d, 'channel_id', 0);
        $reference = trim((string)arr_get($d, 'reference', ''));
        $notes = trim((string)arr_get($d, 'notes', ''));
        $driverName = trim((string)arr_get($d, 'driver_name', ''));
        $vehicle = trim((string)arr_get($d, 'vehicle_unit', ''));
        $routeNo = (int)arr_get($d, 'route_no', 0);
        $routeLabel = trim((string)arr_get($d, 'route_label', ''));
        $departAt = trim((string)arr_get($d, 'depart_at', ''));
        $deliveryDate = trim((string)arr_get($d, 'delivery_date', ''));
        $deliveryTime = trim((string)arr_get($d, 'delivery_time', ''));
        $customerId = (int)arr_get($d, 'customer_id', 0);
        $addressId = (int)arr_get($d, 'address_id', 0);
        $deliveryAddress = trim((string)arr_get($d, 'delivery_address', ''));
        $itemsPayload = arr_get($d, 'items', []);
        if (!is_array($itemsPayload)) $itemsPayload = [];

        if ($channelId <= 0) {
            jexit(['ok' => false, 'error' => 'Canal requerido.'], 400);
        }
        if (!$itemsPayload) {
            jexit(['ok' => false, 'validation_code' => 'LOGISTICS', 'error' => 'Agrega al menos un artículo para crear la orden.'], 400);
        }
        if ($orderType === 'RUTA' && $routeNo <= 0) {
            jexit(['ok' => false, 'error' => 'Selecciona la ruta a cargar para OSA.'], 400);
        }
        if ($orderType === 'CLIENTE') {
            if ($reference === '') jexit(['ok'=>false,'error'=>'Captura la OC del cliente para OSV.'], 400);
            if ($customerId <= 0) jexit(['ok'=>false,'error'=>'Selecciona cliente registrado para OSV.'], 400);
            if ($addressId < 0) $addressId = 0;
        }

        try {
            $customerLabel = '';
            if ($orderType === 'CLIENTE') {
                $stCust = $pdo->prepare("SELECT id, customer_code, business_name, contact_name, address FROM sales_customers WHERE id=? AND company_id=? AND active=1 LIMIT 1");
                $stCust->execute([$customerId, $companyId]);
                $cust = $stCust->fetch(PDO::FETCH_ASSOC);
                if (!$cust) throw new RuntimeException('Cliente inválido o inactivo para OSV.');
                $customerLabel = trim((string)($cust['customer_code'] ?? '') . ((string)($cust['customer_code'] ?? '') !== '' ? ' · ' : '') . (string)($cust['business_name'] ?: $cust['contact_name']));

                if ($addressId > 0) {
                    $hasAddrLocation = column_exists($pdo, 'sales_customer_addresses', 'location_id');
                    $sqlAddr = "SELECT id, address FROM sales_customer_addresses WHERE id=? AND customer_id=? AND active=1";
                    $paramsAddr = [$addressId, $customerId];
                    if ($hasAddrLocation) { $sqlAddr .= " AND (location_id IS NULL OR location_id=?)"; $paramsAddr[] = $locationId; }
                    $sqlAddr .= " LIMIT 1";
                    $stAddr = $pdo->prepare($sqlAddr);
                    $stAddr->execute($paramsAddr);
                    $addr = $stAddr->fetch(PDO::FETCH_ASSOC);
                    if (!$addr) throw new RuntimeException('El punto de entrega no pertenece al cliente o no está activo.');
                    $deliveryAddress = trim((string)$addr['address']);
                } elseif ($deliveryAddress === '') {
                    $deliveryAddress = trim((string)($cust['address'] ?? ''));
                }
                if ($deliveryAddress === '') throw new RuntimeException('Selecciona o captura dirección/punto de entrega para OSV.');
                if ($deliveryDate === '') $deliveryDate = $workDate;
            } else {
                $routeRow = botella_route_row($pdo, $companyId, $locationId, $routeNo);
                $routeLabel = trim(($routeRow['route_code'] ?? ('Ruta ' . $routeNo)) . ' · ' . ($routeRow['route_name'] ?? ''));
                if ($vehicle === '' && !empty($routeRow['vehicle_unit'])) $vehicle = (string)$routeRow['vehicle_unit'];
                if ($driverName === '' && !empty($routeRow['driver_name'])) $driverName = (string)$routeRow['driver_name'];
                if ($deliveryDate === '') $deliveryDate = $workDate;
                if ($reference === '') $reference = 'RUTA-' . str_pad((string)$routeNo, 2, '0', STR_PAD_LEFT) . '-' . str_replace('-', '', $workDate);

                $customerId = botella_route_internal_customer_id($pdo, $companyId, $locationId, $routeNo, $routeRow);
                $deliveryAddress = 'Carga a ruta ' . $routeNo . ($routeLabel !== '' ? ' · ' . $routeLabel : '');
            }

            $comments = [];
            $comments[] = $orderType === 'CLIENTE'
                ? 'Orden OSV creada desde Almacén Botella.'
                : 'Orden OSA de carga a ruta creada desde Almacén Botella.';
            if ($orderType === 'RUTA') $comments[] = 'Ruta DLX: ' . $routeLabel;
            if ($orderType === 'CLIENTE') $comments[] = 'OC cliente: ' . $reference;
            if ($vehicle !== '') $comments[] = 'Unidad a cargar: ' . $vehicle;
            if ($driverName !== '') $comments[] = 'Chofer: ' . $driverName;
            if ($departAt !== '') $comments[] = 'Fecha/hora salida: ' . $departAt;
            if ($notes !== '') $comments[] = 'Notas almacén: ' . $notes;
            $fullComments = implode("\n", $comments);

            $pdo->beginTransaction();
            $st = $pdo->prepare("\n                INSERT INTO sales_orders (\n                    company_id, location_id, customer_id, address_id, source,\n                    requested_delivery_date, requested_delivery_time, route_no,\n                    vehicle_unit, delivery_address, purchase_order, comments,\n                    status, total, approved_by, approved_at, created_by, created_at, updated_at\n                ) VALUES (?, ?, ?, ?, 'VENTAS', ?, ?, ?, ?, ?, ?, ?, 'AUTORIZADA', 0, ?, NOW(), ?, NOW(), NOW())\n            ");
            $st->execute([
                $companyId, $locationId, $customerId, ($orderType === 'CLIENTE' && $addressId > 0 ? $addressId : null),
                $deliveryDate, ($deliveryTime !== '' ? $deliveryTime : null), ($orderType === 'RUTA' ? $routeNo : null),
                ($vehicle !== '' ? $vehicle : null), $deliveryAddress,
                ($reference !== '' ? $reference : null), $fullComments,
                (int)$u['id'], (int)$u['id']
            ]);
            $orderId = (int)$pdo->lastInsertId();
            $folioPrefix = $orderType === 'CLIENTE' ? 'OSV' : 'OSA';
            $folio = $folioPrefix . '-' . str_pad((string)$orderId, 6, '0', STR_PAD_LEFT);
            $pdo->prepare("UPDATE sales_orders SET sales_order_ref=? WHERE id=? AND company_id=? AND location_id=?")
                ->execute([$folio, $orderId, $companyId, $locationId]);

            $hasItemTrace = column_exists($pdo, 'sales_order_items', 'item_customer_id')
                && column_exists($pdo, 'sales_order_items', 'item_address_id')
                && column_exists($pdo, 'sales_order_items', 'customer_label')
                && column_exists($pdo, 'sales_order_items', 'delivery_address')
                && column_exists($pdo, 'sales_order_items', 'purchase_order');

            if ($hasItemTrace) {
                $stItem = $pdo->prepare("\n                    INSERT INTO sales_order_items (\n                        order_id, product_id, purchase_order, item_customer_id, item_address_id, customer_label, delivery_address,\n                        packaging_option_id, qty, packages_qty, pallets_qty,\n                        pallet_type, pallet_specification, unit_price, line_total, notes\n                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?)\n                ");
            } else {
                $stItem = $pdo->prepare("\n                    INSERT INTO sales_order_items (\n                        order_id, product_id, packaging_option_id, qty, packages_qty, pallets_qty,\n                        pallet_type, pallet_specification, unit_price, line_total, notes\n                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?)\n                ");
            }

            /*
             * Si Almacén captura el mismo artículo/embalaje más de una vez para la misma ruta,
             * se consolida en un solo renglón sumando tarimas. Al confirmar la carga, ese total
             * se sumará al inventario existente del almacén móvil RUTA-xx.
             */
            $itemsAgg = [];
            foreach ($itemsPayload as $rawItem) {
                if (!is_array($rawItem)) continue;
                $productId = (int)($rawItem['product_id'] ?? 0);
                $packagingId = (int)($rawItem['packaging_option_id'] ?? 0);
                $palletsRaw = (float)($rawItem['pallets_qty'] ?? 0);
                if ($productId <= 0 || $palletsRaw <= 0) continue;

                $palletType = mb_substr(trim((string)($rawItem['pallet_type'] ?? '')), 0, 40, 'UTF-8');
                $palletSpec = mb_substr(trim((string)($rawItem['pallet_specification'] ?? '')), 0, 60, 'UTF-8');
                $aggKey = $productId . '|' . $packagingId . '|' . mb_strtoupper($palletType, 'UTF-8') . '|' . mb_strtoupper($palletSpec, 'UTF-8');

                if (!isset($itemsAgg[$aggKey])) {
                    $itemsAgg[$aggKey] = [
                        'product_id' => $productId,
                        'packaging_option_id' => $packagingId,
                        'pallets_qty' => 0.0,
                        'pallet_type' => $palletType,
                        'pallet_specification' => $palletSpec,
                    ];
                }
                $itemsAgg[$aggKey]['pallets_qty'] = round((float)$itemsAgg[$aggKey]['pallets_qty'] + $palletsRaw, 6);
            }

            $totalOrderQty = 0.0;
            foreach ($itemsAgg as $rawItem) {
                $productId = (int)($rawItem['product_id'] ?? 0);
                $packagingId = (int)($rawItem['packaging_option_id'] ?? 0);
                $palletsRaw = (float)($rawItem['pallets_qty'] ?? 0);
                if ($productId <= 0 || $palletsRaw <= 0) continue;

                $product = get_product_row($pdo, $companyId, $productId);
                if (!$product) throw new RuntimeException('[LOGISTICS] Producto inválido en artículos.');
                $sku = (string)($product['sku'] ?? ('#' . $productId));
                $packaging = botella_packaging_for_manual_out($pdo, $companyId, $productId, $packagingId);
                $calc = botella_calculate_manual_out($packaging, $palletsRaw, $sku);
                $palletType = mb_substr(trim((string)($rawItem['pallet_type'] ?? '')), 0, 40, 'UTF-8');
                $palletSpec = mb_substr(trim((string)($rawItem['pallet_specification'] ?? '')), 0, 60, 'UTF-8');
                $itemNotes = trim((string)($packaging['packaging_name'] ?? $packaging['packaging_type'] ?? ''));
                if ($itemNotes !== '') $itemNotes = 'Embalaje: ' . $itemNotes;

                if ($hasItemTrace) {
                    $stItem->execute([
                        $orderId, $productId,
                        ($orderType === 'CLIENTE' ? $reference : null),
                        ($orderType === 'CLIENTE' ? $customerId : null),
                        ($orderType === 'CLIENTE' && $addressId > 0 ? $addressId : null),
                        ($orderType === 'CLIENTE' ? $customerLabel : null),
                        ($orderType === 'CLIENTE' ? $deliveryAddress : null),
                        $packagingId > 0 ? $packagingId : null,
                        (float)$calc['qty'], (float)$calc['packages_qty'], (float)$calc['pallets_qty'],
                        $palletType !== '' ? $palletType : null,
                        $palletSpec !== '' ? $palletSpec : null,
                        $itemNotes !== '' ? $itemNotes : null,
                    ]);
                } else {
                    $stItem->execute([
                        $orderId, $productId, $packagingId > 0 ? $packagingId : null,
                        (float)$calc['qty'], (float)$calc['packages_qty'], (float)$calc['pallets_qty'],
                        $palletType !== '' ? $palletType : null,
                        $palletSpec !== '' ? $palletSpec : null,
                        $itemNotes !== '' ? $itemNotes : null,
                    ]);
                }
                $totalOrderQty += (float)$calc['qty'];
            }
            if ($totalOrderQty <= 0.0001) {
                throw new RuntimeException('[LOGISTICS] Agrega al menos un artículo válido para crear la orden.');
            }
            $pdo->prepare("UPDATE sales_orders SET total=? WHERE id=? AND company_id=? AND location_id=?")
                ->execute([$totalOrderQty, $orderId, $companyId, $locationId]);

            if (table_exists($pdo, 'sales_order_status_log')) {
                $stLog = $pdo->prepare("\n                    INSERT INTO sales_order_status_log (order_id, old_status, new_status, changed_by_user_id, changed_by_role, comment, created_at)\n                    VALUES (?, NULL, 'AUTORIZADA', ?, 'ALMACEN', ?, NOW())\n                ");
                $stLog->execute([$orderId, (int)$u['id'], $orderType === 'CLIENTE' ? 'Orden OSV creada desde Almacén; pendiente de carga/salida.' : 'Orden OSA por ruta creada desde Almacén; pendiente de cargar ruta.']);
            }

            $pdo->commit();
            $truckUrl = $orderType === 'RUTA'
                ? ('carga_ruta_botella.php?route_no=' . $routeNo . '&work_date=' . rawurlencode($workDate))
                : ('carga_camion_botella.php?order_id=' . $orderId);
            jexit([
                'ok' => true,
                'id' => $orderId,
                'order_id' => $orderId,
                'folio' => $folio,
                'order_type' => $orderType,
                'route_no' => $orderType === 'RUTA' ? $routeNo : null,
                'route_label' => $routeLabel,
                'truck_url' => $truckUrl,
                'message' => $orderType === 'CLIENTE'
                    ? 'OSV creada desde Almacén. Continúa con el plan de carga del camión.'
                    : 'OSA por ruta creada. Continúa con Cargar ruta para agregar artículos y acomodo.'
            ]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok' => false, 'validation_code' => 'OUT_ERROR', 'error' => $e->getMessage()], 400);
        }
    }




    if ($action === 'return_warehouses') {
        $productId = (int)($_GET['product_id'] ?? 0);
        if ($productId <= 0) {
            jexit(['ok' => false, 'error' => 'product_id requerido'], 400);
        }
        if (!get_product_row($pdo, $companyId, $productId)) {
            jexit(['ok' => false, 'error' => 'Producto inválido.'], 400);
        }

        // Para devoluciones usamos almacenes comerciales configurados para entrada del producto.
        // Se excluyen almacenes internos como MERMA, APROV-MERMA, CUB y SOB-CUB.
        $rows = locreq_stock_by_allowed_wh($pdo, $companyId, $locationId, $productId, 'dest');
        jexit(['ok' => true, 'rows' => $rows]);
    }

    /*
     * Agregar existencias: flujo temporal para plantas sin sección de Producción.
     * Hielo Barra se bloquea tanto visualmente como en backend.
     */
    if ($action === 'manual_stock_in_routes') {
        if (!breport_can_add_existence()) {
            jexit(['ok' => false, 'error' => 'Agregar existencias solo está permitido para Botella, Hielo Bolsa y Garrafón.'], 403);
        }
        try {
            $st = $pdo->prepare("
                SELECT route_no, route_code, route_name, driver_name, vehicle_unit, delivery_days
                FROM route_access_codes
                WHERE company_id = ?
                  AND location_id = ?
                  AND active = 1
                ORDER BY sort_order, route_no, route_name
            ");
            $st->execute([$companyId, $locationId]);
            jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
        } catch (Throwable $e) {
            jexit(['ok' => true, 'rows' => []]);
        }
    }

    if ($action === 'manual_stock_in_packaging_options') {
        if (!breport_can_add_existence()) {
            jexit(['ok' => false, 'error' => 'Agregar existencias solo está permitido para Botella, Hielo Bolsa y Garrafón.'], 403);
        }

        $productId = (int)($_GET['product_id'] ?? 0);
        if ($productId <= 0 || !get_product_row($pdo, $companyId, $productId)) {
            jexit(['ok' => false, 'error' => 'Selecciona un artículo válido.'], 400);
        }

        try {
            jexit(['ok' => true, 'rows' => manual_stock_in_packaging_options($pdo, $companyId, $productId)]);
        } catch (Throwable $e) {
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }
    }

    if ($action === 'manual_stock_in_warehouses') {
        if (!breport_can_add_existence()) {
            jexit(['ok' => false, 'error' => 'Agregar existencias solo está permitido para Botella, Hielo Bolsa y Garrafón.'], 403);
        }

        $productId = (int)($_GET['product_id'] ?? 0);
        if ($productId <= 0 || !get_product_row($pdo, $companyId, $productId)) {
            jexit(['ok' => false, 'error' => 'Selecciona un artículo válido.'], 400);
        }

        try {
            jexit(['ok' => true, 'rows' => manual_stock_in_allowed_warehouses($pdo, $companyId, $locationId, $productId)]);
        } catch (Throwable $e) {
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }
    }

    if ($action === 'manual_stock_in_preview') {
        if (!breport_can_add_existence()) {
            jexit(['ok' => false, 'error' => 'Agregar existencias no está habilitado para esta planta.'], 403);
        }

        try {
            $entry = validate_manual_stock_in_payload($pdo, $companyId, $locationId, read_json());
            $before = get_product_warehouse_available($pdo, $companyId, $locationId, (int)$entry['product_id'], (int)$entry['warehouse_id']);

            jexit([
                'ok' => true,
                'preview' => true,
                'work_date' => $entry['work_date'],
                'product_id' => $entry['product_id'],
                'product_sku' => $entry['product_sku'],
                'product_name' => $entry['product_name'],
                'uom' => $entry['uom'],
                'warehouse_id' => $entry['warehouse_id'],
                'warehouse_code' => $entry['warehouse_code'],
                'warehouse_name' => $entry['warehouse_name'],
                'qty' => $entry['qty'],
                'packaging_name' => $entry['packaging_name'],
                'rack_qty' => $entry['rack_qty'],
                'packages_per_rack' => $entry['packages_per_rack'],
                'loose_packages' => $entry['loose_packages'],
                'total_packages' => $entry['total_packages'],
                'pieces_per_package' => $entry['pieces_per_package'],
                'loose_pieces' => $entry['loose_pieces'],
                'line_name' => $entry['line_name'],
                'provider' => $entry['provider'],
                'route_no' => $entry['route_no'],
                'route_label' => $entry['route_label'],
                'stock_before' => round($before, 3),
                'stock_after' => round($before + (float)$entry['qty'], 3),
                'notes' => $entry['notes'],
            ]);
        } catch (Throwable $e) {
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }
    }

    if ($action === 'manual_stock_in_save') {
        if (!breport_can_add_existence()) {
            jexit(['ok' => false, 'error' => 'Agregar existencias no está habilitado para esta planta.'], 403);
        }

        try {
            $entry = validate_manual_stock_in_payload($pdo, $companyId, $locationId, read_json());

            $pdo->beginTransaction();
            $before = get_product_warehouse_available($pdo, $companyId, $locationId, (int)$entry['product_id'], (int)$entry['warehouse_id']);

            $moveNotes = 'Entrada manual temporal por falta de módulo de producción'
                . ' | Planta: ' . (string)($GLOBALS['scopeCompanyName'] ?? '')
                . ' | Localidad: ' . (string)($GLOBALS['scopeLocationName'] ?? '')
                . ' | Artículo: ' . (string)$entry['product_sku']
                . ' | Almacén: ' . (string)$entry['warehouse_code']
                . ' | Línea: ' . (string)$entry['line_name']
                . ' | Proveedor: ' . (string)$entry['provider']
                . ' | Ruta: ' . (string)$entry['route_label']
                . ' | Racks/Tarimas: ' . (string)$entry['rack_qty']
                . ' | Paq/Rack: ' . (string)$entry['packages_per_rack']
                . ' | Paq sueltos: ' . (string)$entry['loose_packages']
                . ' | Total paquetes: ' . (string)$entry['total_packages']
                . ' | Pz/Paq: ' . (string)$entry['pieces_per_package']
                . ' | Pz sueltas: ' . (string)$entry['loose_pieces']
                . ' | Total piezas: ' . (string)(int)$entry['qty'];
            if ((string)$entry['notes'] !== '') {
                $moveNotes .= ' | Nota: ' . (string)$entry['notes'];
            }

            $st = $pdo->prepare("
                INSERT INTO inventory_moves (
                    company_id, location_id, warehouse_id, work_date,
                    product_id, qty, move_type, channel_id,
                    ref_type, ref_id, notes, created_by, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, 'IN', NULL, 'MANUAL_STOCK_IN', NULL, ?, ?, NOW())
            ");
            $st->execute([
                $companyId,
                $locationId,
                (int)$entry['warehouse_id'],
                (string)$entry['work_date'],
                (int)$entry['product_id'],
                (float)$entry['qty'],
                $moveNotes,
                (int)$u['id'],
            ]);
            $moveId = (int)$pdo->lastInsertId();

            /*
             * Auditoría estructurada opcional.
             * Si aún no existe la tabla botella_stock_receipts, no bloquea el IN real.
             * El movimiento oficial siempre queda en inventory_moves.
             */
            try {
                $stAudit = $pdo->prepare("
                    INSERT INTO botella_stock_receipts (
                        inventory_move_id, company_id, location_id, warehouse_id, product_id,
                        work_date, line_name, provider, route_no, route_label,
                        packaging_option_id, packaging_name,
                        rack_qty, packages_per_rack, loose_packages, total_packages,
                        pieces_per_package, loose_pieces, total_pieces, notes,
                        created_by, created_at
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
                ");
                $stAudit->execute([
                    $moveId, $companyId, $locationId, (int)$entry['warehouse_id'], (int)$entry['product_id'],
                    (string)$entry['work_date'], (string)$entry['line_name'], (string)$entry['provider'],
                    $entry['route_no'], (string)$entry['route_label'],
                    $entry['packaging_option_id'], (string)$entry['packaging_name'],
                    (int)$entry['rack_qty'], (int)$entry['packages_per_rack'], (int)$entry['loose_packages'],
                    (int)$entry['total_packages'], (int)$entry['pieces_per_package'], (int)$entry['loose_pieces'],
                    (int)$entry['qty'], (string)$entry['notes'], (int)$u['id']
                ]);
            } catch (Throwable $auditError) {
                // No se detiene la afectación real si la tabla de auditoría no existe.
            }

            $pdo->commit();

            jexit([
                'ok' => true,
                'inventory_move_id' => $moveId,
                'ref_type' => 'MANUAL_STOCK_IN',
                'work_date' => $entry['work_date'],
                'product_sku' => $entry['product_sku'],
                'product_name' => $entry['product_name'],
                'uom' => $entry['uom'],
                'warehouse_code' => $entry['warehouse_code'],
                'warehouse_name' => $entry['warehouse_name'],
                'qty' => $entry['qty'],
                'packaging_name' => $entry['packaging_name'],
                'rack_qty' => $entry['rack_qty'],
                'packages_per_rack' => $entry['packages_per_rack'],
                'loose_packages' => $entry['loose_packages'],
                'total_packages' => $entry['total_packages'],
                'pieces_per_package' => $entry['pieces_per_package'],
                'loose_pieces' => $entry['loose_pieces'],
                'line_name' => $entry['line_name'],
                'provider' => $entry['provider'],
                'route_no' => $entry['route_no'],
                'route_label' => $entry['route_label'],
                'stock_before' => round($before, 3),
                'stock_after' => round($before + (float)$entry['qty'], 3),
                'notes' => $entry['notes'],
            ]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }
    }


    if ($action === 'warehouse_return_preview') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $d = read_json();
        $workDate    = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate    = breport_resolve_work_date($workDate);
        $reference   = trim((string)arr_get($d, 'reference', ''));
        $productId   = (int)arr_get($d, 'product_id', 0);
        $warehouseId = (int)arr_get($d, 'warehouse_id', 0);
        $qty         = round((float)arr_get($d, 'qty', 0), 3);
        $reason      = trim((string)arr_get($d, 'reason', ''));

        if ($reference === '') {
            jexit(['ok' => false, 'error' => 'Indica la referencia o número de salida original.'], 400);
        }
        if ($productId <= 0 || $warehouseId <= 0 || $qty <= 0) {
            jexit(['ok' => false, 'error' => 'Producto, almacén destino y cantidad son obligatorios.'], 400);
        }
        if ($reason === '') {
            jexit(['ok' => false, 'error' => 'Indica el motivo de la devolución.'], 400);
        }

        $product = get_product_row($pdo, $companyId, $productId);
        if (!$product) {
            jexit(['ok' => false, 'error' => 'Producto inválido.'], 400);
        }

        try {
            locreq_validate_commercial_warehouse($pdo, $companyId, $locationId, $productId, $warehouseId, 'destino', 'dest');
        } catch (Throwable $e) {
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }

        $st = $pdo->prepare("
            SELECT id, code, name
            FROM warehouses
            WHERE id = ?
              AND company_id = ?
              AND location_id = ?
              AND active = 1
            LIMIT 1
        ");
        $st->execute([$warehouseId, $companyId, $locationId]);
        $warehouse = $st->fetch(PDO::FETCH_ASSOC);
        if (!$warehouse) {
            jexit(['ok' => false, 'error' => 'Almacén destino inválido.'], 400);
        }

        $beforeQty = get_product_warehouse_available($pdo, $companyId, $locationId, $productId, $warehouseId);

        jexit([
            'ok' => true,
            'preview' => true,
            'work_date' => $workDate,
            'reference' => $reference,
            'reason' => $reason,
            'qty' => $qty,
            'stock_before' => round($beforeQty, 3),
            'stock_after' => round($beforeQty + $qty, 3),
            'product_id' => $productId,
            'product_sku' => (string)($product['sku'] ?? ''),
            'product_name' => (string)($product['name'] ?? ''),
            'warehouse_id' => $warehouseId,
            'warehouse_code' => (string)($warehouse['code'] ?? ''),
            'warehouse_name' => (string)($warehouse['name'] ?? ''),
        ]);
    }

    if ($action === 'warehouse_return_save') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $d = read_json();
        $workDate    = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate = breport_resolve_work_date($workDate);
        $reference   = trim((string)arr_get($d, 'reference', ''));
        $productId   = (int)arr_get($d, 'product_id', 0);
        $warehouseId = (int)arr_get($d, 'warehouse_id', 0);
        $qty         = round((float)arr_get($d, 'qty', 0), 3);
        $reason      = trim((string)arr_get($d, 'reason', ''));
        $notes       = trim((string)arr_get($d, 'notes', ''));

        if ($reference === '') {
            jexit(['ok' => false, 'error' => 'Indica la referencia o número de salida original.'], 400);
        }
        if ($productId <= 0 || $warehouseId <= 0 || $qty <= 0) {
            jexit(['ok' => false, 'error' => 'Producto, almacén destino y cantidad son obligatorios.'], 400);
        }
        if ($reason === '') {
            jexit(['ok' => false, 'error' => 'Indica el motivo de la devolución.'], 400);
        }

        $product = get_product_row($pdo, $companyId, $productId);
        if (!$product) {
            jexit(['ok' => false, 'error' => 'Producto inválido.'], 400);
        }

        try {
            locreq_validate_commercial_warehouse($pdo, $companyId, $locationId, $productId, $warehouseId, 'destino', 'dest');
        } catch (Throwable $e) {
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }

        $warehouseCode = get_warehouse_code_by_id($pdo, $companyId, $locationId, $warehouseId);
        $stWhReturn = $pdo->prepare("
            SELECT name
            FROM warehouses
            WHERE id = ? AND company_id = ? AND location_id = ? AND active = 1
            LIMIT 1
        ");
        $stWhReturn->execute([$warehouseId, $companyId, $locationId]);
        $warehouseName = (string)($stWhReturn->fetchColumn() ?: $warehouseCode);

        $moveNotes = 'Devolución de almacén | Salida ref: ' . $reference . ' | Motivo: ' . $reason;
        if ($notes !== '') $moveNotes .= ' | ' . $notes;
        $moveNotes = mb_substr($moveNotes, 0, 255);

        $pdo->beginTransaction();
        try {
            /*
             * Saldo del almacén destino inmediatamente antes del movimiento real.
             * El total posterior corresponde a esta devolución IN confirmada.
             */
            $stockBefore = get_product_warehouse_available($pdo, $companyId, $locationId, $productId, $warehouseId);

            $st = $pdo->prepare("\n                INSERT INTO inventory_moves (\n                    company_id, location_id, warehouse_id, work_date, product_id, qty, move_type,\n                    channel_id, ref_type, ref_id, notes, created_by, created_at\n                ) VALUES (\n                    ?, ?, ?, ?, ?, ?, 'IN',\n                    NULL, 'WAREHOUSE_RETURN', NULL, ?, ?, NOW()\n                )\n            ");
            $st->execute([
                $companyId,
                $locationId,
                $warehouseId,
                $workDate,
                $productId,
                $qty,
                $moveNotes,
                (int)$u['id']
            ]);
            $moveId = (int)$pdo->lastInsertId();
            $pdo->commit();
            jexit([
                'ok' => true,
                'inventory_move_id' => $moveId,
                'warehouse_code' => $warehouseCode,
                'warehouse_name' => $warehouseName,
                'product_sku' => (string)($product['sku'] ?? ''),
                'product_name' => (string)($product['name'] ?? ''),
                'qty' => $qty,
                'stock_before' => round($stockBefore, 3),
                'stock_after' => round($stockBefore + $qty, 3),
                'reference' => $reference,
                'reason' => $reason
            ]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            $message = $e->getMessage();
            $validationCode = 'OUT_ERROR';
            if (str_starts_with($message, '[PALLET_DECIMAL]')) {
                $validationCode = 'PALLET_DECIMAL';
                $message = str_replace('[PALLET_DECIMAL] ', '', $message);
            } elseif (str_starts_with($message, '[LOGISTICS]')) {
                $validationCode = 'LOGISTICS';
                $message = str_replace('[LOGISTICS] ', '', $message);
            }
            jexit(['ok' => false, 'validation_code' => $validationCode, 'error' => $message], 400);
        }
    }

    if ($action === 'transfer_save') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        if (!column_exists($pdo, 'transfers', 'from_warehouse_id') || !column_exists($pdo, 'transfers', 'to_warehouse_id')) {
            jexit(['ok' => false, 'error' => 'Faltan columnas from_warehouse_id y to_warehouse_id en transfers.'], 500);
        }

        $d = read_json();

        $workDate        = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate = breport_resolve_work_date($workDate);
        $productId       = (int)arr_get($d, 'product_id', 0);
        $qty             = (float)arr_get($d, 'qty', 0);
        $fromWarehouseId = (int)arr_get($d, 'from_warehouse_id', 0);
        $toWarehouseId   = (int)arr_get($d, 'to_warehouse_id', 0);
        $notes           = trim((string)arr_get($d, 'notes', ''));

        if ($productId <= 0 || $qty <= 0) {
            jexit(['ok' => false, 'error' => 'Producto y cantidad son obligatorios.'], 400);
        }
        if ($fromWarehouseId <= 0 || $toWarehouseId <= 0) {
            jexit(['ok' => false, 'error' => 'Almacén origen y destino son obligatorios.'], 400);
        }
        if ($fromWarehouseId === $toWarehouseId) {
            jexit(['ok' => false, 'error' => 'El almacén origen y destino no pueden ser iguales.'], 400);
        }

        $st = $pdo->prepare("
            SELECT id
            FROM warehouses
            WHERE company_id = ? AND location_id = ? AND active = 1
              AND id IN (?, ?)
        ");
        $st->execute([$companyId, $locationId, $fromWarehouseId, $toWarehouseId]);
        if (count($st->fetchAll(PDO::FETCH_ASSOC)) !== 2) {
            jexit(['ok' => false, 'error' => 'Warehouse origen o destino inválido.'], 400);
        }

        try {
            validate_internal_transfer_rule($pdo, $companyId, $locationId, $productId, $fromWarehouseId, $toWarehouseId);
        } catch (Throwable $e) {
            jexit(['ok' => false, 'error' => $e->getMessage()], 400);
        }

        $availableInFrom = get_product_warehouse_available($pdo, $companyId, $locationId, $productId, $fromWarehouseId);
        if ($qty > $availableInFrom) {
            jexit(['ok' => false, 'error' => 'Inventario insuficiente en el almacén origen. Disponible: ' . number_format($availableInFrom, 3, '.', '')], 409);
        }

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("
                INSERT INTO transfers (
                    from_company_id, from_location_id, from_warehouse_id,
                    to_company_id, to_location_id, to_warehouse_id,
                    transfer_date, status, notes, created_by, created_at, received_at
                ) VALUES (
                    ?, ?, ?,
                    ?, ?, ?,
                    ?, 'RECEIVED', ?, ?, NOW(), NOW()
                )
            ");
            $st->execute([
                $companyId, $locationId, $fromWarehouseId,
                $companyId, $locationId, $toWarehouseId,
                $workDate,
                ($notes !== '' ? $notes : 'Transferencia interna entre almacenes'),
                (int)$u['id']
            ]);
            $transferId = (int)$pdo->lastInsertId();

            $stItem = $pdo->prepare("
                INSERT INTO transfer_items (
                    transfer_id, product_id, qty, conversion_to_product_id, conversion_factor
                ) VALUES (?, ?, ?, NULL, NULL)
            ");
            $stItem->execute([$transferId, $productId, $qty]);

            $transferNotes = $notes !== '' ? $notes : 'Transferencia interna entre almacenes';
            $stMove = $pdo->prepare("
                INSERT INTO inventory_moves (
                    company_id, location_id, warehouse_id, work_date, product_id, qty, move_type,
                    channel_id, ref_type, ref_id, notes, created_by, created_at
                ) VALUES (
                    ?, ?, ?, ?, ?, ?, ?,
                    NULL, 'TRANSFER', ?, ?, ?, NOW()
                )
            ");
            $stMove->execute([
                $companyId,
                $locationId,
                $fromWarehouseId,
                $workDate,
                $productId,
                $qty,
                'OUT',
                $transferId,
                'Transferencia enviada #' . $transferId . ' | ' . $transferNotes,
                (int)$u['id']
            ]);
            $stMove->execute([
                $companyId,
                $locationId,
                $toWarehouseId,
                $workDate,
                $productId,
                $qty,
                'IN',
                $transferId,
                'Transferencia recibida #' . $transferId . ' | ' . $transferNotes,
                (int)$u['id']
            ]);

            $pdo->commit();
            jexit(['ok' => true, 'id' => $transferId]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok' => false, 'error' => $e->getMessage()], 500);
        }
    }

    if ($action === 'delete') {
        if (!$canWrite) {
            jexit(['ok' => false, 'error' => 'Sin permiso.'], 403);
        }

        $d = read_json();
        $id = (int)arr_get($d, 'id', 0);
        if ($id <= 0) {
            jexit(['ok' => false, 'error' => 'ID inválido.'], 400);
        }

        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare("
                SELECT ref_type, ref_id
                FROM inventory_moves
                WHERE id = ? AND company_id = ? AND location_id = ?
                LIMIT 1
            ");
            $st->execute([$id, $companyId, $locationId]);
            $m = $st->fetch(PDO::FETCH_ASSOC);

            if (!$m) {
                throw new RuntimeException('Movimiento no encontrado.');
            }

            $refType = (string)($m['ref_type'] ?? '');
            $refId   = (int)($m['ref_id'] ?? 0);

            if ($refType === 'OUTBOUND' && $refId > 0) {
                $st = $pdo->prepare("
                    DELETE FROM inventory_moves
                    WHERE company_id = ? AND location_id = ?
                      AND ref_type = 'OUTBOUND' AND ref_id = ?
                ");
                $st->execute([$companyId, $locationId, $refId]);

                $st = $pdo->prepare("DELETE FROM outbound_items WHERE outbound_id = ?");
                $st->execute([$refId]);

                $st = $pdo->prepare("
                    DELETE FROM outbounds
                    WHERE id = ? AND company_id = ? AND location_id = ?
                ");
                $st->execute([$refId, $companyId, $locationId]);
            } else {
                $st = $pdo->prepare("
                    DELETE FROM inventory_moves
                    WHERE id = ? AND company_id = ? AND location_id = ?
                ");
                $st->execute([$id, $companyId, $locationId]);
            }

            $pdo->commit();
            jexit(['ok' => true]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            jexit(['ok' => false, 'error' => $e->getMessage()], 500);
        }
    }

    jexit(['ok' => false, 'error' => 'Acción no válida.'], 404);
}

$csrf = csrf_token();
?>
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>BReport · Almacén Botella</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">

  <style>
  :root{
    --bg1:#f4f7fb;
    --bg2:#eaf0f7;
    --line:#d9e3ef;
    --txt:#203247;
    --txt-strong:#10243e;
    --txt-soft:#5e7188;
    --gold:#d6a542;
    --gold-soft:#f9edd1;
    --green:#128b4f;
    --danger:#c63b50;
    --blue:#1f6fff;
    --shadow:0 14px 36px rgba(14, 30, 56, .08);
    --radius:24px;

    --field-bg:#ffffff;
    --field-bg-hover:#ffffff;
    --field-bg-focus:#ffffff;
    --field-border:#d4deea;
    --field-border-focus:rgba(31,111,255,.35);
    --field-text:#1d3047;
    --field-placeholder:#7a8ea6;
    --field-disabled:#eef3f8;
  }

  *{box-sizing:border-box}
  html,body{min-height:100%}

  html{
    color-scheme:light;
  }

  body{
    margin:0;
    font-family:'Inter',system-ui,Arial,sans-serif;
    color:var(--txt);
    background:
      radial-gradient(circle at 12% 20%, rgba(35,120,255,.08), transparent 24%),
      radial-gradient(circle at 88% 80%, rgba(0,190,255,.06), transparent 20%),
      linear-gradient(180deg, #f8fbff 0%, var(--bg1) 55%, var(--bg2) 100%);
    background-attachment:fixed;
  }

  .topbar{
    background:linear-gradient(135deg,#082a5b,#11498f);
    backdrop-filter:blur(12px);
    border-bottom:1px solid rgba(255,255,255,.10);
    box-shadow:0 10px 26px rgba(12,32,69,.18);
    position:sticky;
    top:0;
    z-index:1040;
  }

  .page-wrap{ padding:28px 18px 40px; }
  .shell{ max-width:1480px; margin:0 auto; }
  .panel-pad{ padding:22px; }

  .brand-title{
    font-weight:800;
    font-size:1.9rem;
    color:var(--txt-strong);
  }

  .brand-sub,.muted,.kpi-label,.kpi-hint,.small,.small-note,td .muted{
    color:var(--txt-soft)!important;
  }

  .glass{
    background:#ffffff;
    border:1px solid var(--line);
    border-radius:var(--radius);
    box-shadow:var(--shadow);
    backdrop-filter:none;
  }

  .section-title,.fw-bold,.fw-semibold,.kpi-value,.table thead th{
    color:var(--txt-strong);
  }

  .kpi-card{
    min-height:130px;
    border-radius:22px;
    border:1px solid #e1e9f2;
    background:#ffffff;
    box-shadow:0 10px 24px rgba(16,36,62,.06);
    transition:.18s ease;
  }

  .kpi-card:hover{ transform:translateY(-3px); }

  .kpi-label{
    color:#b8ccef!important;
    font-size:.95rem;
    font-weight:700;
  }

  .kpi-value{
    color:#ffffff!important;
    font-size:2.1rem;
    font-weight:800;
    letter-spacing:.3px;
  }

  .kpi-hint{
    color:#c4d5f1!important;
    font-size:.88rem;
  }

  .badge-soft{
    display:inline-flex;
    align-items:center;
    gap:8px;
    padding:8px 14px;
    border-radius:999px;
    background:rgba(244,190,89,.14);
    color:#ffd978;
    border:1px solid rgba(244,190,89,.32);
    font-weight:800;
  }

  .product-mini-card{
    min-height:110px;
    border-radius:20px;
    border:1px solid rgba(160,198,255,.16);
    background:linear-gradient(180deg, rgba(8,25,60,.96), rgba(4,18,42,.94));
    box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 18px 40px rgba(0,0,0,.18);
    transition:.18s ease;
  }

  .product-mini-card:hover{
    transform:translateY(-3px);
  }

  .product-mini-title{
    color:#b8ccef!important;
    font-size:.92rem;
    font-weight:700;
    line-height:1.2;
  }

  .product-mini-value{
    color:#ffffff!important;
    font-size:1.9rem;
    font-weight:800;
    line-height:1;
  }

  .product-mini-sub{
    color:#c4d5f1!important;
    font-size:.84rem;
  }

  .product-mini-bar{
    border-color:rgba(244,190,89,.28);
    box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 18px 40px rgba(0,0,0,.18);
  }

  .small-note{
    border-radius:16px;
    border:1px solid rgba(244,190,89,.25);
    background:rgba(244,190,89,.10);
    color:var(--gold-soft)!important;
    padding:12px 14px;
    font-size:.95rem;
    font-weight:600;
    line-height:1.45;
  }

  .pulse-soft{ animation:pulseSoft 3.2s ease-in-out infinite; }

  @keyframes pulseSoft{
    0%,100%{transform:scale(1)}
    50%{transform:scale(1.01)}
  }

  .table-wrap{
    border:1px solid rgba(162,198,255,.12);
    border-radius:22px;
    overflow:hidden;
    background:rgba(5,20,47,.52);
  }

  .table{
    margin:0;
    color:var(--txt);
    --bs-table-bg:transparent;
    --bs-table-striped-bg:rgba(255,255,255,.02);
    --bs-table-hover-bg:rgba(68,124,220,.10);
    --bs-table-border-color:rgba(169,203,255,.08);
  }

  .table thead th{
    color:#f8fbff!important;
    font-weight:800;
    font-size:1rem;
    background:rgba(12,31,71,.82);
    white-space:nowrap;
    border-bottom:1px solid rgba(169,203,255,.12);
    padding-top:1rem;
    padding-bottom:1rem;
  }

  .table tbody td{
    color:#eaf2ff!important;
    vertical-align:middle;
    border-color:rgba(169,203,255,.08);
  }

  .mono{
    font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
    font-variant-numeric:tabular-nums;
    color:#f4f8ff!important;
    letter-spacing:.2px;
  }

  .text-warning,.text-gold{ color:#ffd978!important; }
  .text-success,.text-green{ color:#7cf0ae!important; }
  .text-danger{ color:#ff96a8!important; }
  .text-blue{ color:#8bc8ff!important; }

  .form-label{
    color:#c9d9f4;
    font-weight:700;
    font-size:.92rem;
  }

  .form-control,
  .form-select,
  textarea,
  input[type="text"],
  input[type="number"],
  input[type="date"],
  input[type="datetime-local"]{
    min-height:48px;
    border-radius:16px;
    color:var(--field-text)!important;
    background:var(--field-bg)!important;
    background-color:var(--field-bg)!important;
    border:1px solid var(--field-border)!important;
    box-shadow:none!important;
    caret-color:#ffffff;
  }

  textarea.form-control{
    min-height:auto;
  }

  .form-control::placeholder,
  textarea::placeholder,
  input::placeholder{
    color:var(--field-placeholder)!important;
    opacity:1;
  }

  .form-control:hover,
  .form-select:hover,
  textarea:hover,
  input:hover{
    background:var(--field-bg-hover)!important;
    background-color:var(--field-bg-hover)!important;
    color:var(--field-text)!important;
  }

  .form-control:focus,
  .form-select:focus,
  textarea:focus,
  input[type="text"]:focus,
  input[type="number"]:focus,
  input[type="date"]:focus,
  input[type="datetime-local"]:focus{
    background:var(--field-bg-focus)!important;
    background-color:var(--field-bg-focus)!important;
    color:#ffffff!important;
    border-color:var(--field-border-focus)!important;
    box-shadow:0 0 0 .22rem rgba(87,165,255,.18)!important;
    outline:none!important;
  }

  .form-control:disabled,
  .form-select:disabled,
  textarea:disabled,
  input:disabled{
    background:var(--field-disabled)!important;
    background-color:var(--field-disabled)!important;
    color:#b8c8e4!important;
    opacity:1!important;
  }

  .form-select option{
    background:#143369!important;
    color:#ffffff!important;
  }

  input[type="date"],
  input[type="datetime-local"],
  input[type="number"]{
    color-scheme:light;
    color:var(--field-text)!important;
  }

  input[type="date"]::-webkit-calendar-picker-indicator,
  input[type="datetime-local"]::-webkit-calendar-picker-indicator{
    filter:invert(1) brightness(1.15);
    opacity:.95;
    cursor:pointer;
  }

  input[type="date"]::-webkit-datetime-edit,
  input[type="datetime-local"]::-webkit-datetime-edit,
  input[type="date"]::-webkit-datetime-edit-text,
  input[type="datetime-local"]::-webkit-datetime-edit-text,
  input[type="date"]::-webkit-datetime-edit-month-field,
  input[type="datetime-local"]::-webkit-datetime-edit-month-field,
  input[type="date"]::-webkit-datetime-edit-day-field,
  input[type="datetime-local"]::-webkit-datetime-edit-day-field,
  input[type="date"]::-webkit-datetime-edit-year-field,
  input[type="datetime-local"]::-webkit-datetime-edit-year-field,
  input[type="datetime-local"]::-webkit-datetime-edit-hour-field,
  input[type="datetime-local"]::-webkit-datetime-edit-minute-field{
    color:var(--field-text)!important;
  }

  input:-webkit-autofill,
  input:-webkit-autofill:hover,
  input:-webkit-autofill:focus,
  textarea:-webkit-autofill,
  textarea:-webkit-autofill:hover,
  textarea:-webkit-autofill:focus,
  select:-webkit-autofill,
  select:-webkit-autofill:hover,
  select:-webkit-autofill:focus{
    -webkit-text-fill-color:#ffffff!important;
    -webkit-box-shadow:0 0 0 1000px var(--field-bg-focus) inset!important;
    box-shadow:0 0 0 1000px var(--field-bg-focus) inset!important;
    transition:background-color 9999s ease-in-out 0s;
  }

  input::selection,
  textarea::selection{
    background:rgba(87,165,255,.35);
    color:#ffffff;
  }

  .nav-btn,.toolbar-btn{
    border-radius:14px;
    border:1px solid rgba(160,199,255,.22);
    background:rgba(15,37,81,.74);
    color:#f7fbff!important;
    font-weight:600;
    transition:all .18s ease;
  }

  .nav-btn:hover,.toolbar-btn:hover{
    transform:translateY(-1px);
    color:#fff!important;
    background:rgba(28,53,104,.92);
  }

  .btn-green-soft{
    color:#fff!important;
    font-weight:700;
    border:none;
    border-radius:18px;
    background:linear-gradient(135deg, #149f5e, #24cd7a);
    box-shadow:0 12px 32px rgba(20,159,94,.24);
  }

  .btn-blue-soft{
    color:#fff!important;
    font-weight:700;
    border:none;
    border-radius:18px;
    background:linear-gradient(135deg, #1e63ff, #3898ff);
    box-shadow:0 12px 32px rgba(37,113,255,.28);
  }

  .btn-gold-soft{
    color:#fff!important;
    font-weight:700;
    border:none;
    border-radius:18px;
    background:linear-gradient(135deg, #b9851f, #f0bb4d);
    box-shadow:0 12px 32px rgba(185,133,31,.24);
  }

  .modal-content{
    background:linear-gradient(180deg, rgba(7,24,58,.98), rgba(4,18,44,.98));
    border:1px solid rgba(161,198,255,.16);
    color:#fff;
    border-radius:24px;
  }

  .modal-header,.modal-footer{
    border-color:rgba(161,198,255,.12);
  }

  .item-row{
    padding:14px;
    border-radius:18px;
    border:1px solid rgba(160,197,255,.12);
    background:rgba(255,255,255,.04);
  }

  .status-chip{
    display:inline-flex;
    align-items:center;
    justify-content:center;
    min-width:112px;
    padding:8px 14px;
    border-radius:999px;
    font-weight:800;
    font-size:.82rem;
    letter-spacing:.32px;
    border:1px solid transparent;
    text-transform:uppercase;
  }

  .status-autorizada{
    background:rgba(114,240,165,.14);
    color:#7cf0ae;
    border-color:rgba(114,240,165,.24);
  }

  .status-surtiendo{
    background:rgba(244,190,89,.14);
    color:#ffd978;
    border-color:rgba(244,190,89,.24);
  }

  #emptyHint{ color:#b7cae8!important; }

  @media (max-width: 991.98px){
    .brand-title{font-size:1.45rem;}
    .kpi-value{font-size:1.7rem;}
    .page-wrap{padding:18px 12px 28px;}
    .panel-pad{padding:16px;}
    .small-note{width:100%;}
  }

  .brand-title{ color:#ffffff; }
  .brand-sub,.topbar .muted,.topbar .small{ color:rgba(255,255,255,.78)!important; }
  .section-title,.fw-bold,.fw-semibold,.kpi-value,.table thead th,.modal-title,.table td,.table th,label,.form-label,.item-row,.small-note,.modal-content,.modal-header,.modal-body,.modal-footer{ color:var(--txt-strong); }
  .muted,.kpi-label,.kpi-hint,.small,.small-note,td .muted,.text-muted{ color:var(--txt-soft)!important; }

  .table-wrap{
    background:#ffffff;
    border:1px solid var(--line);
    border-radius:20px;
    overflow:hidden;
  }
  .table{
    --bs-table-bg:#ffffff;
    --bs-table-color:#203247;
    --bs-table-striped-color:#203247;
    --bs-table-hover-color:#10243e;
    --bs-table-hover-bg:#f5f9ff;
    margin-bottom:0;
  }
  .table thead th{
    background:#1e3357!important;
    color:#ffffff!important;
    border-color:#2c4672!important;
  }
  .table td{
    border-color:#e7edf5!important;
    color:#203247!important;
    vertical-align:middle;
  }
  .table tbody tr:nth-child(even){ background:#fbfdff; }
  .table tbody tr:hover{ background:#f5f9ff!important; }

  .product-mini-card{
    background:#ffffff;
    border:1px solid #dce5f0;
    box-shadow:0 10px 24px rgba(16,36,62,.06);
  }
  .product-mini-title{ color:#10243e!important; }
  .product-mini-sub{ color:#6a7d93!important; }
  .product-mini-value{ color:#2563eb!important; }
  .badge-soft{
    background:#edf4ff;
    color:#1f5ec8;
    border:1px solid #d4e3fb;
  }

  .toolbar-btn{
    background:#ffffff;
    color:#17345c;
    border:1px solid #d6e0eb;
  }
  .toolbar-btn:hover{ background:#f4f8fd; color:#10243e; }
  .btn-gold-soft{
    background:#fff4d9;
    color:#8d6400;
    border:1px solid #ecc667;
  }
  .btn-gold-soft:hover{ background:#f8e6b4; color:#6e4f00; }
  .btn-blue-soft{
    background:#eaf2ff;
    color:#1f5ec8;
    border:1px solid #bcd2fb;
  }
  .btn-blue-soft:hover{ background:#dce9ff; color:#184fae; }
  .btn-green-soft{
    background:#e9f8ef;
    color:#0f7a44;
    border:1px solid #bfe7cd;
  }
  .btn-green-soft:hover{ background:#d8f1e2; color:#0d6638; }

  .small-note{
    background:#f6f9fc;
    border:1px solid #e2eaf3;
    color:#5f7289!important;
  }

  .item-row{
    background:#ffffff;
    border:1px solid #e3ebf3;
    color:#10243e;
    box-shadow:0 8px 20px rgba(16,36,62,.05);
  }

  .form-control,.form-select{
    background:#ffffff!important;
    color:#1d3047!important;
    border:1px solid #d4deea!important;
  }
  .form-control::placeholder{ color:#7a8ea6!important; }
  .form-control:focus,.form-select:focus{
    background:#ffffff!important;
    color:#1d3047!important;
    border-color:#81a8ef!important;
    box-shadow:0 0 0 .2rem rgba(31,111,255,.12)!important;
  }
  .form-control:disabled,.form-select:disabled{ background:#eef3f8!important; color:#7588a0!important; }

  .modal-content{
    background:#ffffff;
    border:1px solid #d9e3ef;
    color:#1f3148;
    box-shadow:0 18px 36px rgba(13,32,64,.16);
  }
  .modal-header{
    background:linear-gradient(135deg,#082a5b,#11498f);
    color:#ffffff;
    border-bottom:1px solid rgba(255,255,255,.10);
  }
  .modal-header .fw-bold,.modal-header .modal-title,.modal-header .small,.modal-header .muted{ color:#ffffff!important; }
  .btn-close-white{ filter:none; }
  .modal-body,.modal-footer{ background:#ffffff; }
  .modal-footer{ border-top:1px solid #e6edf5; }

  .status-chip{ font-weight:700; }
  .status-autorizada{ background:#eaf7ee; color:#137a45; border:1px solid #bee4cc; }

  #detailNote{
    background:#f6f9fc;
    border:1px solid #e2eaf3;
    border-radius:18px;
    padding:14px 16px;
    color:#5f7289!important;
  }

  .hero-divider{ display:none; }
  .inventory-table-section{ display:none; }




/* ============================================================
   OVERRIDE FINAL · TEMA CLARO LEGIBLE PARA ALMACÉN
   ------------------------------------------------------------
   Mantiene banner azul y fuerza textos oscuros sobre fondos claros.
   Evita herencias del tema oscuro anterior.
============================================================ */
:root{
  --mes-bg:#f4f7fb;
  --mes-panel:#ffffff;
  --mes-panel-2:#f8fbff;
  --mes-line:#dbe6f2;
  --mes-line-soft:#edf2f7;
  --mes-text:#10243e;
  --mes-muted:#5e7188;
  --mes-blue:#0f4f95;
  --mes-blue-2:#123f73;
  --mes-accent:#1f6fff;
  --mes-green:#128b4f;
  --mes-gold:#b7791f;
  --mes-danger:#c63b50;
}

html{ color-scheme:light!important; }
body{
  color:var(--mes-text)!important;
  background:
    radial-gradient(circle at 12% 20%, rgba(31,111,255,.06), transparent 22%),
    linear-gradient(180deg,#f9fbff 0%,#f4f7fb 100%)!important;
}

/* Banner superior azul con textos claros */
.topbar{
  background:linear-gradient(135deg,#0b2848,#145798)!important;
  border-bottom:1px solid rgba(255,255,255,.16)!important;
  color:#ffffff!important;
}
.topbar *,
.topbar .brand-title,
.topbar .brand-sub{
  color:#ffffff!important;
}
.topbar .btn,
.topbar a.btn{
  color:#ffffff!important;
  border-color:rgba(255,255,255,.45)!important;
}
.topbar .btn:hover{
  background:rgba(255,255,255,.14)!important;
}

/* Contenedores principales */
.glass,
.card,
.modal-content,
.kpi-card,
.product-mini-card,
.product-mini-bar,
.item-row,
.table-wrap{
  background:#ffffff!important;
  background-color:#ffffff!important;
  color:var(--mes-text)!important;
  border-color:var(--mes-line)!important;
  box-shadow:0 14px 34px rgba(16,36,62,.08)!important;
}

.panel-pad,
.card-body,
.modal-body,
.modal-header,
.modal-footer{
  background:#ffffff!important;
  color:var(--mes-text)!important;
}

/* Títulos y textos */
.brand-title:not(.topbar .brand-title),
.section-title,
.fw-bold,
.fw-semibold,
.kpi-value,
.product-mini-value,
.product-mini-title,
.product-mini-sub,
.modal-title,
.item-row div,
td,
th,
label,
.form-label,
.mono{
  color:var(--mes-text)!important;
}

.brand-sub:not(.topbar .brand-sub),
.muted,
.text-muted,
.kpi-label,
.kpi-hint,
.small,
.small-note,
.product-mini-sub,
td .muted,
.item-row .small{
  color:var(--mes-muted)!important;
}

/* KPIs claros */
.kpi-card{
  min-height:130px;
  border-radius:22px;
}
.kpi-label{
  color:var(--mes-muted)!important;
  font-weight:800!important;
}
.kpi-value{
  color:var(--mes-text)!important;
  font-size:2.1rem;
  font-weight:900!important;
}
.kpi-hint{
  color:var(--mes-muted)!important;
}

.product-mini-card{
  border-radius:20px!important;
}
.product-mini-title{
  color:var(--mes-text)!important;
  font-weight:800!important;
}
.product-mini-value{
  color:#0b2848!important;
}
.product-mini-sub{
  color:var(--mes-muted)!important;
}

/* Notas informativas */
.small-note,
.badge-soft{
  background:#fff7e6!important;
  border:1px solid #f3d39b!important;
  color:#7c4a03!important;
}

/* Tablas */
.table-wrap{
  overflow:hidden;
  border-radius:22px!important;
}
.table{
  color:var(--mes-text)!important;
  --bs-table-bg:#ffffff!important;
  --bs-table-striped-bg:#f8fbff!important;
  --bs-table-hover-bg:#eef6ff!important;
  --bs-table-border-color:#e3ecf6!important;
}
.table thead th{
  background:#eef4fb!important;
  color:var(--mes-text)!important;
  border-bottom:1px solid var(--mes-line)!important;
}
.table tbody td{
  color:var(--mes-text)!important;
  background:#ffffff!important;
  border-color:#e7eef7!important;
}
.table tbody tr:hover td{
  background:#f4f9ff!important;
}
.table .mono,
td.mono,
td .mono{
  color:var(--mes-text)!important;
}

/* Modales claros */
.modal-content{
  border-radius:24px!important;
  overflow:hidden;
}
.modal-header{
  border-bottom:1px solid var(--mes-line)!important;
}
.modal-footer{
  border-top:1px solid var(--mes-line)!important;
}
.modal-backdrop.show{
  opacity:.48!important;
}

/* Formularios */
.form-control,
.form-select,
textarea,
input[type="text"],
input[type="number"],
input[type="date"],
input[type="datetime-local"]{
  background:#ffffff!important;
  background-color:#ffffff!important;
  color:var(--mes-text)!important;
  border:1px solid #ccd9e8!important;
  border-radius:14px!important;
  caret-color:var(--mes-text)!important;
  box-shadow:none!important;
}
.form-control:focus,
.form-select:focus,
textarea:focus,
input:focus{
  background:#ffffff!important;
  color:var(--mes-text)!important;
  border-color:rgba(31,111,255,.55)!important;
  box-shadow:0 0 0 .22rem rgba(31,111,255,.12)!important;
}
.form-control::placeholder,
textarea::placeholder,
input::placeholder{
  color:#8aa0b8!important;
}
.form-select option{
  background:#ffffff!important;
  color:var(--mes-text)!important;
}
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator{
  filter:none!important;
}

/* Botones */
.btn-outline-light{
  color:#ffffff!important;
  border-color:rgba(255,255,255,.45)!important;
}
.btn-outline-primary{
  color:#165dff!important;
  border-color:#9fc0ff!important;
  background:#ffffff!important;
}
.btn-outline-primary:hover{
  color:#ffffff!important;
  background:#165dff!important;
  border-color:#165dff!important;
}
.btn-outline-secondary,
.btn-outline-dark{
  color:var(--mes-text)!important;
  border-color:#cdd9e7!important;
  background:#ffffff!important;
}
.btn-outline-secondary:hover,
.btn-outline-dark:hover{
  color:var(--mes-text)!important;
  background:#eef4fb!important;
}
.btn-outline-danger{
  color:var(--mes-danger)!important;
  border-color:#f0b5c0!important;
  background:#ffffff!important;
}
.btn-outline-danger:hover{
  color:#ffffff!important;
  background:var(--mes-danger)!important;
}

/* Colores semánticos legibles */
.text-warning,
.text-gold{
  color:#a16207!important;
}
.text-success,
.text-green{
  color:#0f7a43!important;
}
.text-danger{
  color:#b42335!important;
}
.text-blue{
  color:#145798!important;
}

/* Badges Bootstrap en tema claro */
.badge.text-bg-warning{
  color:#5f3b00!important;
}
.badge.text-bg-light{
  color:var(--mes-text)!important;
  border:1px solid var(--mes-line)!important;
}

/* Scrollbar suave */
*::-webkit-scrollbar{ width:10px; height:10px; }
*::-webkit-scrollbar-thumb{
  background:#c4d3e4;
  border-radius:999px;
}
*::-webkit-scrollbar-track{
  background:#eef3f8;
}



/* ============================================================
   OVERRIDE FINAL · BOTONES VISIBLES Y ACCIONES CLARAS
   ------------------------------------------------------------
   Refuerza contraste de botones en fondo blanco.
============================================================ */
.btn{
  border-radius:14px!important;
  font-weight:800!important;
  letter-spacing:.01em;
  opacity:1!important;
  box-shadow:0 8px 18px rgba(16,36,62,.08)!important;
}
.btn:disabled,
.btn.disabled{
  opacity:.58!important;
  cursor:not-allowed!important;
  filter:none!important;
}

/* Botones principales */
.btn-primary,
.btn.btn-primary{
  background:#165dff!important;
  border-color:#165dff!important;
  color:#ffffff!important;
}
.btn-primary:hover{
  background:#0f4ecf!important;
  border-color:#0f4ecf!important;
  color:#ffffff!important;
}

.btn-success,
.btn.btn-success{
  background:#128b4f!important;
  border-color:#128b4f!important;
  color:#ffffff!important;
}
.btn-success:hover{
  background:#0f743f!important;
  border-color:#0f743f!important;
  color:#ffffff!important;
}

.btn-warning,
.btn.btn-warning{
  background:#f59e0b!important;
  border-color:#d97706!important;
  color:#1f1300!important;
}
.btn-warning:hover{
  background:#d97706!important;
  border-color:#b45309!important;
  color:#ffffff!important;
}

/* Botones outline visibles */
.btn-outline-primary,
.btn.btn-outline-primary{
  background:#ffffff!important;
  border:1.5px solid #165dff!important;
  color:#0f4ecf!important;
}
.btn-outline-primary:hover,
.btn-outline-primary:focus{
  background:#165dff!important;
  border-color:#165dff!important;
  color:#ffffff!important;
}

.btn-outline-success,
.btn.btn-outline-success{
  background:#ffffff!important;
  border:1.5px solid #128b4f!important;
  color:#0f743f!important;
}
.btn-outline-success:hover,
.btn-outline-success:focus{
  background:#128b4f!important;
  border-color:#128b4f!important;
  color:#ffffff!important;
}

.btn-outline-warning,
.btn.btn-outline-warning{
  background:#fff8e8!important;
  border:1.5px solid #d97706!important;
  color:#7c3f00!important;
}
.btn-outline-warning:hover,
.btn-outline-warning:focus{
  background:#d97706!important;
  border-color:#b45309!important;
  color:#ffffff!important;
}

.btn-outline-secondary,
.btn.btn-outline-secondary,
.btn-outline-dark,
.btn.btn-outline-dark{
  background:#ffffff!important;
  border:1.5px solid #64748b!important;
  color:#10243e!important;
}
.btn-outline-secondary:hover,
.btn-outline-dark:hover,
.btn-outline-secondary:focus,
.btn-outline-dark:focus{
  background:#10243e!important;
  border-color:#10243e!important;
  color:#ffffff!important;
}

.btn-outline-danger,
.btn.btn-outline-danger{
  background:#ffffff!important;
  border:1.5px solid #dc2626!important;
  color:#b91c1c!important;
}
.btn-outline-danger:hover,
.btn-outline-danger:focus{
  background:#dc2626!important;
  border-color:#dc2626!important;
  color:#ffffff!important;
}

/* Barra de acciones superior */
.glass .btn,
.card .btn{
  min-height:38px;
}
.glass .btn-sm,
.card .btn-sm,
.table .btn-sm{
  min-height:30px;
  padding:.38rem .68rem!important;
  font-size:.82rem!important;
}

/* Botones en tablas: más visibles */
.table .btn,
.table .btn-sm{
  opacity:1!important;
  font-weight:800!important;
  box-shadow:0 4px 12px rgba(16,36,62,.10)!important;
}
.table .btn-outline-warning,
.table .btn-warning{
  background:#fff3cf!important;
  border-color:#d97706!important;
  color:#6b3a00!important;
}
.table .btn-outline-warning:hover,
.table .btn-warning:hover{
  background:#d97706!important;
  color:#ffffff!important;
}

/* Botón detalle barra */
#btnDetail,
button[id*="Detail"],
button[data-sales-detail]{
  opacity:1!important;
}

/* Estado deshabilitado: visible, pero claramente inactivo */
button:disabled,
.btn:disabled{
  background:#eef3f8!important;
  border-color:#c8d6e6!important;
  color:#6b7f96!important;
  box-shadow:none!important;
}

/* Íconos dentro de botones */
.btn i{
  color:inherit!important;
}

/* En el encabezado de la tarjeta de almacén, separa más las acciones */
.card-actions,
.action-buttons,
.d-flex.gap-2.ms-auto{
  gap:.55rem!important;
  flex-wrap:wrap!important;
}

/* FORZAR TEXTO OSCURO EN BOTONES */
.btn,
.btn *,
button,
button * {
  color: #10243e !important;
}

/* Botones con fondo sólido deben quedar blancos */
.btn-primary,
.btn-primary *,
.btn-success,
.btn-success *,
.btn-danger,
.btn-danger * {
  color: #ffffff !important;
}

/* Botones deshabilitados visibles */
.btn:disabled,
button:disabled,
.btn:disabled *,
button:disabled * {
  color: #10243e !important;
  opacity: 1 !important;
}

/* Botones superiores tipo outline */
.btn-outline-primary,
.btn-outline-secondary,
.btn-outline-warning,
.btn-outline-success,
.btn-outline-dark,
.btn-outline-primary *,
.btn-outline-secondary *,
.btn-outline-warning *,
.btn-outline-success *,
.btn-outline-dark * {
  color: #10243e !important;
}


/* ============================================================
   Cards de navegación de almacén
============================================================ */
.module-card{
  border:1px solid #d8e6f4;
  border-radius:22px;
  background:linear-gradient(180deg,#ffffff,#f8fbff);
  padding:18px;
  min-height:150px;
  cursor:pointer;
  transition:transform .16s ease, box-shadow .16s ease, border-color .16s ease;
  box-shadow:0 14px 30px rgba(15,35,65,.08);
  color:#14243a!important;
}
.module-card:hover{transform:translateY(-2px);box-shadow:0 20px 44px rgba(15,35,65,.13);border-color:#9fc4ec;}
.module-card.active{border-color:#2f80ed;box-shadow:0 18px 42px rgba(47,128,237,.18);}
.module-icon{
  width:46px;height:46px;border-radius:16px;display:flex;align-items:center;justify-content:center;
  background:#eaf3ff;color:#175fc5;font-size:1.35rem;margin-bottom:12px;
}
.module-title{font-weight:800;color:#10243e;font-size:1.05rem;}
.module-sub{font-size:.88rem;color:#64748b;margin-top:4px;min-height:42px;}
.module-count{font-weight:900;color:#175fc5;font-size:1.6rem;line-height:1;margin-top:10px;}
.module-section{display:none;}
.module-section.active{display:block;}

.history-summary-card{
  border:1px solid #d8e6f4;
  border-radius:22px;
  background:linear-gradient(180deg,#ffffff,#f8fbff);
  padding:18px;
  min-height:142px;
  cursor:pointer;
  transition:transform .16s ease, box-shadow .16s ease, border-color .16s ease;
  box-shadow:0 14px 30px rgba(15,35,65,.08);
}
.history-summary-card:hover{
  transform:translateY(-3px);
  box-shadow:0 20px 44px rgba(15,35,65,.14);
  border-color:#9fc4ec;
}
.history-summary-title{
  color:#10243e;
  font-weight:900;
  line-height:1.2;
}
.history-summary-sub{
  color:#64748b;
  font-size:.86rem;
  margin-top:2px;
}
.history-summary-value{
  font-size:1.85rem;
  font-weight:950;
  line-height:1;
}
.history-summary-value.in{color:#128b4f!important;}
.history-summary-value.out{color:#c63b50!important;}
.history-summary-pill{
  display:inline-flex;
  align-items:center;
  gap:.35rem;
  border-radius:999px;
  padding:.28rem .62rem;
  font-size:.75rem;
  font-weight:900;
}
.history-summary-pill.in{background:#dcfce7;color:#166534;border:1px solid #bbf7d0;}
.history-summary-pill.out{background:#fee2e2;color:#991b1b;border:1px solid #fecaca;}
.product-move-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:14px;}
.product-move-stat{border-radius:14px;border:1px solid rgba(15,35,65,.08);padding:10px;background:#fff;}
.product-move-label{font-size:.72rem;font-weight:800;color:#6a7d93;text-transform:uppercase;letter-spacing:.02em;}
.product-move-value{font-size:1.05rem;font-weight:900;line-height:1.1;}
.product-move-value.in{color:#128b4f;}
.product-move-value.current{color:#2563eb;}
.product-move-value.out{color:#c63b50;}
.product-mini-card[data-inventory-product]{cursor:pointer;}
.mh-warehouse-code{color:#2563eb;font-weight:900;}
@media (max-width:575.98px){.product-move-grid{grid-template-columns:1fr;}}

.history-filter-box{
  border:1px solid #dbe7f5;
  border-radius:22px;
  background:#fff;
  padding:16px;
  box-shadow:0 12px 28px rgba(15,35,65,.07);
}



/* ===== Rutas de órdenes creadas desde ventas ===== */
.route-filter-card{border:1px solid var(--line);border-radius:22px;background:linear-gradient(180deg,#ffffff,#f8fbff);padding:18px;box-shadow:0 14px 32px rgba(15,35,65,.08)}
.route-card{width:100%;text-align:left;border:1px solid #dbe7f5;border-radius:24px;padding:18px;background:#fff;box-shadow:0 14px 30px rgba(15,35,65,.08);transition:.18s ease;min-height:172px}
.route-card:hover{transform:translateY(-3px);box-shadow:0 20px 42px rgba(15,35,65,.14)}
.route-card.route-pending{border-color:#fecaca;background:linear-gradient(180deg,#fff7f7,#fff)}
.route-card.route-done{border-color:#bbf7d0;background:linear-gradient(180deg,#f0fff6,#fff)}
.route-card.route-mixed{border-color:#fde68a;background:linear-gradient(180deg,#fffbeb,#fff)}
.route-badge{display:inline-flex;align-items:center;gap:.4rem;border-radius:999px;padding:.28rem .68rem;font-size:.78rem;font-weight:800}
.route-badge.pending{background:#fee2e2;color:#991b1b;border:1px solid #fecaca}
.route-badge.done{background:#dcfce7;color:#166534;border:1px solid #bbf7d0}
.route-badge.mixed{background:#fef3c7;color:#92400e;border:1px solid #fde68a}
.route-number{font-size:1.8rem;font-weight:900;color:#14375f;line-height:1}
.route-meta{font-size:.86rem;color:#60748e}
.route-order-card{border:1px solid #dbe7f5;border-radius:20px;background:#fff;padding:16px;box-shadow:0 10px 24px rgba(15,35,65,.07)}
.route-order-card.pending{border-color:#fecaca;background:#fff8f8}
.route-order-card.done{border-color:#bbf7d0;background:#f7fff9}
.route-empty{border:1px dashed #b9cbe2;border-radius:22px;padding:28px;text-align:center;background:#fbfdff;color:#60748e}



/* ============================================================
   FIX FINAL · INVENTARIO UNIFICADO RESPONSIVO Y COLORES
   ------------------------------------------------------------
   Corrige herencia de .mono y mantiene el orden visual en desktop,
   tablet y celular. No afecta lógica operativa ni movimientos.
============================================================ */
#productCardsRow{
  align-items:stretch!important;
}
#productCardsRow > [class*="col-"]{
  display:flex!important;
}
#productCardsRow .product-mini-card{
  width:100%!important;
  min-height:174px!important;
  display:flex!important;
  flex-direction:column!important;
  justify-content:space-between!important;
  border:1px solid #d7e4f2!important;
  box-shadow:0 12px 28px rgba(16,36,62,.08)!important;
}
#productCardsRow .product-move-grid{
  display:grid!important;
  grid-template-columns:repeat(4,minmax(0,1fr))!important;
  gap:8px!important;
  margin-top:14px!important;
}
#productCardsRow .product-move-stat{
  background:#ffffff!important;
  border:1px solid #dfe8f3!important;
  border-radius:14px!important;
  padding:10px 8px!important;
  min-width:0!important;
}
#productCardsRow .product-move-label{
  color:#60748e!important;
  font-size:.70rem!important;
  font-weight:900!important;
  text-transform:uppercase!important;
  letter-spacing:.02em!important;
}
#productCardsRow .product-move-value,
#productCardsRow .product-move-value.mono{
  font-size:1.14rem!important;
  font-weight:950!important;
  line-height:1.05!important;
  letter-spacing:.01em!important;
}
#productCardsRow .product-move-value.initial,
#productCardsRow .product-move-value.initial.mono{
  color:#92400e!important;
}
#productCardsRow .product-move-value.in,
#productCardsRow .product-move-value.in.mono{
  color:#0f8a4b!important;
}
#productCardsRow .product-move-value.current,
#productCardsRow .product-move-value.current.mono{
  color:#0b5ed7!important;
}
#productCardsRow .product-move-value.out,
#productCardsRow .product-move-value.out.mono{
  color:#dc2626!important;
}
#moduleCards{
  align-items:stretch!important;
}
#moduleCards > [class*="col-"]{
  display:flex!important;
}
#moduleCards .module-card{
  width:100%!important;
}


.operation-nav{
  padding:14px 16px;
  border:1px solid #dbe6f2;
  border-radius:20px;
  background:#ffffff;
  box-shadow:0 12px 28px rgba(16,36,62,.06);
}
.operation-nav-btn{
  display:inline-flex;
  align-items:center;
  gap:7px;
  padding:10px 14px;
  border-radius:12px;
  border:1px solid #cfe0f4;
  background:#f7fbff;
  color:#1b3857;
  font-weight:800;
  transition:all .16s ease;
}
.operation-nav-btn:hover,
.operation-nav-btn.active{
  color:#ffffff;
  background:#1765d1;
  border-color:#1765d1;
  box-shadow:0 8px 18px rgba(23,101,209,.18);
}
.operation-nav-btn .op-count{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-width:22px;
  height:22px;
  padding:0 7px;
  border-radius:999px;
  background:#e8f1ff;
  color:#0f4c81;
  font-size:.76rem;
  font-weight:950;
  line-height:1;
}
.operation-nav-btn.active .op-count,
.operation-nav-btn:hover .op-count{
  background:rgba(255,255,255,.22);
  color:#fff;
}
#dispatchMailboxDate:placeholder-shown{
  color:#7d90a8;
}
.dispatch-filter-note{
  font-size:.8rem;
  color:#64748b;
  margin-top:4px;
}

#dispatchMailboxSection .dispatch-mailbox{
  display:grid;
  grid-template-columns:240px minmax(300px, 410px) minmax(360px, 1fr);
  min-height:560px;
  border:1px solid #dbe6f2;
  border-radius:22px;
  overflow:hidden;
  background:#fff;
}
.dispatch-folders{
  padding:16px 12px;
  background:#f5f9fe;
  border-right:1px solid #dbe6f2;
}
.dispatch-folder{
  width:100%;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:10px;
  border:0;
  border-radius:14px;
  padding:12px 10px;
  margin-bottom:8px;
  background:transparent;
  color:#19324d;
  font-weight:800;
  text-align:left;
}
.dispatch-folder.active,
.dispatch-folder:hover{
  color:#1153c7;
  background:#e8f1ff;
}
.dispatch-folder-count{
  display:inline-flex;
  min-width:29px;
  height:25px;
  align-items:center;
  justify-content:center;
  padding:0 8px;
  border-radius:999px;
  background:#e3ebf6;
  color:#19324d;
  font-size:.82rem;
}
.dispatch-folder.active .dispatch-folder-count{
  background:#2563eb;
  color:#fff;
}
.dispatch-suggestion{
  margin-top:20px;
  padding:12px;
  border:1px solid #c9dcf6;
  border-radius:14px;
  background:#eef6ff;
  font-size:.84rem;
  color:#3f5874;
}
.dispatch-list{
  border-right:1px solid #dbe6f2;
  min-width:0;
  background:#fff;
}
.dispatch-list-head{
  padding:16px;
  border-bottom:1px solid #e6edf5;
}
.dispatch-items{
  max-height:500px;
  overflow:auto;
}
.dispatch-item{
  width:100%;
  padding:15px 16px;
  border:0;
  border-bottom:1px solid #eef3f8;
  background:#fff;
  text-align:left;
  color:#10243e;
}
.dispatch-item:hover,
.dispatch-item.active{
  background:#f1f7ff;
}
.dispatch-item.active{box-shadow:inset 3px 0 0 #2563eb;}
.dispatch-item-title{font-weight:800; color:#122b48;}
.dispatch-item-sub{font-size:.84rem;color:#64748b;margin-top:4px;}
.dispatch-item-bottom{
  display:flex;
  justify-content:space-between;
  gap:10px;
  margin-top:8px;
  font-size:.82rem;
  color:#586e88;
}
.dispatch-detail{
  padding:22px;
  min-width:0;
  background:#fff;
}
.dispatch-empty{
  min-height:370px;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
  color:#64748b;
  padding:30px;
}
.dispatch-detail-card{
  border:1px solid #e1ebf5;
  border-radius:16px;
  background:#f8fbff;
  padding:13px 15px;
}
.dispatch-detail-label{
  color:#64748b;
  text-transform:uppercase;
  font-weight:800;
  font-size:.72rem;
  letter-spacing:.03em;
}
.dispatch-detail-value{
  color:#10243e;
  font-weight:800;
  margin-top:4px;
}
.dispatch-status{
  display:inline-flex;
  align-items:center;
  padding:5px 11px;
  border-radius:999px;
  font-weight:800;
  font-size:.75rem;
}
.dispatch-status.pending{background:#fff3d6;color:#8a5b04;}
.dispatch-status.done{background:#def8e9;color:#087443;}
.dispatch-status.manual{background:#e8f1ff;color:#1153c7;}
@media (max-width:1199.98px){
  #dispatchMailboxSection .dispatch-mailbox{
    grid-template-columns:220px 1fr;
  }
  .dispatch-detail{
    grid-column:1 / -1;
    border-top:1px solid #dbe6f2;
  }
}
@media (max-width:767.98px){
  #dispatchMailboxSection .dispatch-mailbox{display:block;min-height:0;}
  .dispatch-folders{
    border-right:0;
    border-bottom:1px solid #dbe6f2;
    display:flex;
    flex-wrap:wrap;
    gap:8px;
  }
  .dispatch-folder{width:auto;margin:0;}
  .dispatch-suggestion{width:100%;margin-top:8px;}
  .dispatch-list{border-right:0;}
  .dispatch-items{max-height:380px;}
}

@media (max-width:991.98px){
  .shell{max-width:100%!important;}
  .panel-pad{padding:18px!important;}
}
@media (max-width:575.98px){
  .page-wrap{padding:14px 10px 28px!important;}
  #productCardsRow .product-mini-card{min-height:0!important;}
  #productCardsRow .product-move-grid{
    grid-template-columns:repeat(4,minmax(0,1fr))!important;
    gap:6px!important;
  }
  #productCardsRow .product-move-stat{padding:8px 6px!important;}
  #productCardsRow .product-move-label{font-size:.62rem!important;}
  #productCardsRow .product-move-value,
  #productCardsRow .product-move-value.mono{font-size:.98rem!important;}
  .history-filter-box{display:grid!important;grid-template-columns:1fr 1fr!important;gap:10px!important;}
  .history-filter-box .btn{width:100%!important;}
  .history-filter-box .small-note{grid-column:1 / -1!important;margin-left:0!important;}
}

.scope-pill{
  display:inline-flex;
  align-items:center;
  gap:.4rem;
  border:1px solid rgba(148,163,184,.35);
  background:rgba(255,255,255,.78);
  color:#334155;
  border-radius:999px;
  padding:.38rem .72rem;
  font-size:.82rem;
  font-weight:700;
  white-space:nowrap;
}
.scope-select{
  width:auto!important;
  min-width:190px!important;
  max-width:260px!important;
  border-radius:999px!important;
  font-weight:700!important;
  color:#0f172a!important;
  background:#ffffff!important;
  border:1px solid rgba(148,163,184,.45)!important;
}
@media(max-width:768px){
  .scope-select{width:100%!important;max-width:none!important;}
  .scope-pill{width:100%;justify-content:center;}
}

/* ============================================================
   FIX RESPONSIVO · TARJETAS DE INVENTARIO POR PLANTA
   ------------------------------------------------------------
   El grid de productos debe vivir fuera del flex de filtros.
   Estas reglas evitan tarjetas angostas y mantienen lectura profesional.
============================================================ */
#productCardsRow{
  display:flex!important;
  flex-wrap:wrap!important;
  align-items:stretch!important;
  width:100%!important;
  margin-left:0!important;
  margin-right:0!important;
}
#productCardsRow > .inventory-product-col{
  display:flex!important;
  min-width:0!important;
}
#productCardsRow .product-mini-card{
  width:100%!important;
  min-width:0!important;
  overflow:hidden!important;
  background:linear-gradient(180deg,#ffffff,#f8fbff)!important;
  border:1px solid #d8e6f4!important;
  border-radius:22px!important;
  color:#10243e!important;
}
#productCardsRow .product-mini-title{
  color:#10243e!important;
  font-size:1rem!important;
  font-weight:900!important;
  line-height:1.18!important;
  overflow-wrap:anywhere!important;
}
#productCardsRow .product-mini-sub{
  color:#64748b!important;
  font-size:.88rem!important;
  line-height:1.35!important;
  overflow-wrap:anywhere!important;
}
#productCardsRow .product-move-grid{
  grid-template-columns:repeat(4,minmax(84px,1fr))!important;
}
#productCardsRow .product-move-stat{
  min-width:0!important;
  text-align:left!important;
}
#productCardsRow .product-move-label{
  white-space:nowrap!important;
  overflow:hidden!important;
  text-overflow:ellipsis!important;
}
#productCardsRow .product-move-value{
  white-space:nowrap!important;
  overflow:hidden!important;
  text-overflow:ellipsis!important;
}
.product-warehouse-list{
  display:flex!important;
  flex-wrap:wrap!important;
  gap:6px!important;
  margin-top:8px!important;
}
.product-warehouse-pill{
  display:inline-flex!important;
  align-items:center!important;
  gap:5px!important;
  max-width:100%!important;
  padding:4px 8px!important;
  border-radius:999px!important;
  border:1px solid #cfe0f4!important;
  background:#eef6ff!important;
  color:#17446f!important;
  font-size:.78rem!important;
  font-weight:800!important;
}
.product-warehouse-pill.neg{
  background:#fff1f2!important;
  color:#9f1239!important;
  border-color:#fecdd3!important;
}
@media (max-width:1199.98px){
  #productCardsRow .product-move-grid{
    grid-template-columns:repeat(2,minmax(0,1fr))!important;
  }
}
@media (max-width:575.98px){
  #productCardsRow{
    gap:10px 0!important;
  }
  #productCardsRow .product-mini-card{
    border-radius:18px!important;
  }
  #productCardsRow .product-move-grid{
    grid-template-columns:repeat(2,minmax(0,1fr))!important;
  }
}


  /* ============================================================
     Corrección visual de modales · Botella
     Fondo claro, texto oscuro, encabezado azul y controles legibles.
     ============================================================ */
  .modal-content{
    background:#ffffff!important;
    color:#1f3148!important;
    border:1px solid #d9e3ef!important;
    border-radius:24px!important;
    box-shadow:0 24px 64px rgba(13,32,64,.22)!important;
    overflow:hidden;
  }
  .modal-header{
    background:linear-gradient(135deg,#082a5b,#11498f)!important;
    color:#ffffff!important;
    border-bottom:1px solid rgba(255,255,255,.14)!important;
  }
  .modal-header .fw-bold,
  .modal-header .fs-5,
  .modal-header .modal-title,
  .modal-header .small,
  .modal-header .muted{
    color:#ffffff!important;
  }
  .modal-body,
  .modal-footer{
    background:#ffffff!important;
    color:#1f3148!important;
  }
  .modal-footer{
    border-top:1px solid #e6edf5!important;
  }
  .modal-body .form-label,
  .modal-body label,
  .modal-body .fw-bold,
  .modal-body .fw-semibold,
  .modal-body .section-title{
    color:#10243e!important;
  }
  .modal-body .small,
  .modal-body .muted,
  .modal-body .text-muted{
    color:#5f7289!important;
  }
  .modal-body .form-control,
  .modal-body .form-select,
  .modal-body textarea{
    background:#ffffff!important;
    color:#1d3047!important;
    border:1px solid #cdd9e8!important;
    box-shadow:none!important;
  }
  .modal-body .form-control::placeholder,
  .modal-body textarea::placeholder{
    color:#8093aa!important;
  }
  .modal-body .form-control:focus,
  .modal-body .form-select:focus,
  .modal-body textarea:focus{
    background:#ffffff!important;
    color:#1d3047!important;
    border-color:#4f8cff!important;
    box-shadow:0 0 0 .22rem rgba(79,140,255,.16)!important;
  }
  .modal-body .form-select option{
    background:#ffffff!important;
    color:#1d3047!important;
  }
  .btn-close-white{
    filter:invert(1) grayscale(100%) brightness(200%)!important;
    opacity:.95!important;
  }
  .modal-body .table-wrap{
    background:#ffffff!important;
    border:1px solid #dbe5f1!important;
  }
  .modal-body .table thead th{
    background:#eaf2ff!important;
    color:#10243e!important;
    border-color:#dbe5f1!important;
  }
  .modal-body .table tbody td{
    background:#ffffff!important;
    color:#1f3148!important;
    border-color:#e6edf5!important;
  }
  .modal-body .mono{
    color:#10243e!important;
  }



/* ============================================================
   CABECERO COMPACTO PARA TABLET / CELULAR
   ------------------------------------------------------------
   En pantallas pequeñas el encabezado azul no debe ocupar toda
   la vista. Se compactan título, subtítulo, selector y botones.
============================================================ */
@media (max-width: 991.98px){
  .topbar{
    position:sticky!important;
    top:0!important;
    padding:.42rem 0!important;
  }
  .topbar .container-fluid{
    display:grid!important;
    grid-template-columns:1fr!important;
    gap:.45rem!important;
    align-items:start!important;
    padding-left:.75rem!important;
    padding-right:.75rem!important;
  }
  .topbar .brand-title{
    font-size:1.12rem!important;
    line-height:1.12!important;
    margin:0!important;
    letter-spacing:-.02em!important;
    white-space:nowrap!important;
    overflow:hidden!important;
    text-overflow:ellipsis!important;
  }
  .topbar .brand-sub{
    display:none!important;
  }
  .topbar .ms-auto{
    width:100%!important;
    margin-left:0!important;
    display:grid!important;
    grid-template-columns:1fr 1fr 1fr!important;
    gap:.38rem!important;
    align-items:center!important;
  }
  .topbar .ms-auto > .muted.small{
    display:none!important;
  }
  .topbar .scope-pill{
    grid-column:1 / -1!important;
    width:100%!important;
    justify-content:center!important;
    min-height:32px!important;
    padding:.28rem .55rem!important;
    font-size:.72rem!important;
    line-height:1.1!important;
    background:rgba(255,255,255,.16)!important;
    color:#ffffff!important;
    border-color:rgba(255,255,255,.22)!important;
  }
  .topbar .scope-pill b,
  .topbar .scope-pill i{
    color:#ffffff!important;
  }
  .topbar .scope-select{
    grid-column:1 / -1!important;
    width:100%!important;
    max-width:none!important;
    min-width:0!important;
    min-height:34px!important;
    height:34px!important;
    padding:.25rem .75rem!important;
    font-size:.82rem!important;
    border-radius:14px!important;
  }
  .topbar .nav-btn,
  .topbar a.nav-btn{
    width:100%!important;
    min-height:34px!important;
    padding:.35rem .48rem!important;
    font-size:.78rem!important;
    line-height:1!important;
    border-radius:14px!important;
    display:inline-flex!important;
    align-items:center!important;
    justify-content:center!important;
    gap:.25rem!important;
    white-space:nowrap!important;
  }
  .topbar .nav-btn i,
  .topbar a.nav-btn i{
    margin-right:0!important;
  }
  .page-wrap{
    padding-top:12px!important;
  }
}

@media (max-width: 575.98px){
  .topbar .container-fluid{
    gap:.35rem!important;
  }
  .topbar .brand-title{
    font-size:1rem!important;
  }
  .topbar .scope-pill{
    display:none!important;
  }
  .topbar .scope-select{
    display:none!important;
  }
  .topbar .ms-auto{
    grid-template-columns:repeat(4,minmax(0,1fr))!important;
    gap:.32rem!important;
  }
  .topbar #btnScopeChange{
    display:none!important;
  }
  .topbar .nav-btn,
  .topbar a.nav-btn{
    min-height:31px!important;
    padding:.3rem .34rem!important;
    font-size:.72rem!important;
    border-radius:12px!important;
  }
  .page-wrap{
    padding:10px 8px 26px!important;
  }
}

    .capacity-mini{border:1px solid #d8e3f0;background:#f8fbff;border-radius:16px;padding:11px 14px;}
    .route-items-wrap{display:grid;gap:12px;}
    .route-items-wrap .item-row{border:1px solid #d8e3f0;border-radius:18px;background:#fbfdff;padding:14px;}
    .route-items-wrap .item-row:hover{border-color:#b8d3ff;box-shadow:0 8px 24px rgba(15,76,129,.08);}


/* Monitor flotante compacto de pedidos por surtir */
.pending-watch-float{
  position:fixed;
  right:22px;
  bottom:22px;
  z-index:2200;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:8px;
  border:0;
  border-radius:999px;
  padding:10px 14px;
  background:linear-gradient(135deg,#0f4c81,#1d75d8);
  color:#fff;
  box-shadow:0 14px 32px rgba(15,76,129,.28);
  font-weight:950;
  min-width:auto;
  width:auto;
  white-space:nowrap;
  line-height:1;
}
.pending-watch-float .watch-icon{
  width:30px;height:30px;border-radius:999px;
  background:rgba(255,255,255,.18);
  display:flex;align-items:center;justify-content:center;
  font-size:1rem;
  flex:0 0 auto;
}
.pending-watch-float .watch-main{line-height:1;display:flex;align-items:center;gap:5px}
.pending-watch-float .watch-count{font-size:.94rem;letter-spacing:-.02em;display:flex;align-items:center;gap:4px}
.pending-watch-float .watch-timer{display:none!important}
.pending-watch-float.has-new{animation:pendingPulse 1.1s ease-in-out infinite;background:linear-gradient(135deg,#f59e0b,#ef4444)}
.pending-watch-float.has-new .watch-icon{background:rgba(255,255,255,.24)}
@keyframes pendingPulse{
  0%,100%{transform:translateY(0) scale(1);box-shadow:0 14px 32px rgba(239,68,68,.28)}
  50%{transform:translateY(-3px) scale(1.04);box-shadow:0 20px 44px rgba(239,68,68,.42)}
}
.pending-watch-toast{
  position:fixed;
  right:22px;
  bottom:78px;
  z-index:2201;
  width:min(390px,calc(100vw - 28px));
  background:#fff;
  color:#10243e;
  border:1px solid #bfdbfe;
  border-left:6px solid #1d75d8;
  border-radius:20px;
  box-shadow:0 18px 45px rgba(15,35,65,.22);
  padding:14px 16px;
  display:none;
}
.pending-watch-toast.show{display:block;animation:toastIn .22s ease-out both}
.pending-watch-toast .toast-title{font-weight:950;margin-bottom:4px}
.pending-watch-toast .toast-body{font-size:.9rem;color:#475569}
@keyframes toastIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
@media(max-width:575.98px){
  .pending-watch-float{right:14px;bottom:14px;left:auto;width:auto;min-width:auto;justify-content:center;border-radius:999px;padding:10px 13px}
  .pending-watch-toast{right:12px;bottom:74px;width:calc(100vw - 24px)}
}


/* ============================================================
   DISEÑO MEJORADO · CENTRO OPERATIVO TIPO BOTONES
   ------------------------------------------------------------
   Botones grandes, táctiles y responsivos para Almacén Botella.
============================================================ */
.warehouse-command-center{
  padding:20px!important;
  border:1px solid #d8e6f4!important;
  border-radius:26px!important;
  background:
    radial-gradient(circle at top right, rgba(29,117,189,.12), transparent 35%),
    linear-gradient(180deg,#ffffff,#f8fbff)!important;
  box-shadow:0 20px 46px rgba(15,35,65,.10)!important;
}
.command-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px;}
.command-eyebrow{font-size:.78rem;font-weight:950;text-transform:uppercase;letter-spacing:.06em;color:#1d75bd;margin-bottom:3px;}
.command-title{font-size:1.28rem;font-weight:950;color:#10243e;line-height:1.15;letter-spacing:-.02em;}
.command-sub{font-size:.9rem;color:#64748b;margin-top:4px;max-width:720px;}
.command-live-pill{display:inline-flex;align-items:center;gap:7px;border-radius:999px;padding:.5rem .75rem;background:#eef6ff;border:1px solid #cfe0f4;color:#0f4c81;font-weight:900;font-size:.82rem;white-space:nowrap;}
.warehouse-action-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;}
.warehouse-action-card{appearance:none;text-align:left;border:1px solid #d7e4f2;border-radius:22px;background:#fff;min-height:132px;padding:16px;display:grid;grid-template-columns:auto 1fr auto;gap:12px;align-items:start;cursor:pointer;color:#12233d;box-shadow:0 14px 30px rgba(15,35,65,.08);transition:transform .16s ease,box-shadow .16s ease,border-color .16s ease,background .16s ease;position:relative;overflow:hidden;}
.warehouse-action-card:after{content:"";position:absolute;right:-34px;bottom:-34px;width:92px;height:92px;border-radius:999px;background:rgba(29,117,189,.07);pointer-events:none;}
.warehouse-action-card:hover{transform:translateY(-3px);box-shadow:0 22px 46px rgba(15,35,65,.14);border-color:#9fc4ec;background:linear-gradient(180deg,#fff,#f8fbff);}
.warehouse-action-card.active{border-color:#1765d1;background:linear-gradient(135deg,#0b2f55,#1765d1);color:#fff;box-shadow:0 20px 44px rgba(23,101,209,.22);}
.warehouse-action-card.active .wa-content small{color:rgba(255,255,255,.82)!important;}
.warehouse-action-card:disabled{opacity:.55;cursor:not-allowed;transform:none!important;}
.warehouse-action-card{text-decoration:none!important;color:#0f172a;}
.wa-icon{width:44px;height:44px;border-radius:16px;display:flex;align-items:center;justify-content:center;font-size:1.28rem;box-shadow:inset 0 1px 0 rgba(255,255,255,.22);}
.wa-blue{background:#eaf3ff;color:#175fc5}.wa-purple{background:#f3e8ff;color:#7e22ce}.wa-green{background:#dcfce7;color:#166534}.wa-indigo{background:#e0e7ff;color:#3730a3}.wa-cyan{background:#cffafe;color:#0e7490}.wa-gold{background:#fef3c7;color:#92400e}.wa-orange{background:#ffedd5;color:#c2410c}.wa-dark{background:#e2e8f0;color:#0f172a}.warehouse-action-card.active .wa-icon{background:rgba(255,255,255,.18)!important;color:#fff!important;border:1px solid rgba(255,255,255,.18)}
.wa-content{display:block;min-width:0;position:relative;z-index:1}.wa-content strong{display:block;font-size:1rem;font-weight:950;line-height:1.18}.wa-content small{display:block;color:#64748b;font-size:.82rem;line-height:1.25;margin-top:5px}.wa-arrow{position:relative;z-index:1;align-self:center;color:#7c8da5;font-size:1rem}.warehouse-action-card.active .wa-arrow{color:#fff}.wa-count{position:relative;z-index:1;align-self:start;justify-self:end;min-width:28px;height:28px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;padding:0 8px;background:#1765d1;color:#fff;font-weight:950;font-size:.8rem}.warehouse-action-card.active .wa-count{background:rgba(255,255,255,.22);color:#fff;}
@media(max-width:1199.98px){.warehouse-action-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.command-head{display:block}.command-live-pill{margin-top:10px}}
@media(max-width:575.98px){.warehouse-command-center{padding:14px!important;border-radius:22px!important}.command-title{font-size:1.08rem}.command-sub{font-size:.82rem}.warehouse-action-grid{grid-template-columns:1fr;gap:10px}.warehouse-action-card{min-height:98px;border-radius:18px;padding:14px}.wa-icon{width:40px;height:40px;border-radius:14px}.wa-content strong{font-size:.96rem}.wa-content small{font-size:.78rem}}
.warehouse-context-note{display:flex;align-items:center;gap:9px;border:1px solid #dbeafe;background:#f8fbff;color:#0f3b63;border-radius:16px;padding:10px 12px;font-size:.88rem;font-weight:750;max-width:520px}.warehouse-context-note i{color:#1d75bd;font-size:1.05rem}.page-wrap .warehouse-command-center:first-child{margin-top:0!important;margin-bottom:18px!important}.warehouse-command-center.top-priority{border-color:#b9d8f7!important;box-shadow:0 22px 55px rgba(15,59,99,.14)!important}
.inventory-modal-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.inventory-modal-kpi{border:1px solid #dbeafe;background:#f8fbff;border-radius:16px;padding:12px 14px}.inventory-modal-kpi span{display:block;color:#64748b;font-size:.78rem;font-weight:800;text-transform:uppercase;letter-spacing:.03em}.inventory-modal-kpi strong{display:block;color:#08264a;font-size:1.45rem;font-weight:950;line-height:1.05;margin-top:4px}.inventory-modal-empty{border:1px dashed #cbd5e1;border-radius:18px;padding:18px;text-align:center;color:#64748b;background:#f8fafc}@media(max-width:991.98px){.inventory-modal-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:575.98px){.inventory-modal-summary{grid-template-columns:1fr}.inventory-modal-kpi strong{font-size:1.25rem}}


.route-stock-panel{
  margin-top:1rem;
  padding:1rem;
  border:1px solid #bfdbfe;
  background:linear-gradient(135deg,#f8fbff,#eef6ff);
  border-radius:18px;
  box-shadow:0 10px 24px rgba(15,59,99,.08);
}
.route-stock-totals{display:flex;gap:.5rem;flex-wrap:wrap}
.route-stock-totals span{
  display:inline-flex;align-items:center;gap:.25rem;
  background:#fff;border:1px solid #dbeafe;border-radius:999px;
  padding:.38rem .65rem;font-size:.83rem;color:#0f3b63
}
.route-stock-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.6rem}
.route-stock-item{
  background:#fff;border:1px solid #dbe3ee;border-radius:14px;
  padding:.7rem .75rem;min-width:0
}
.route-stock-item .sku{font-weight:900;color:#0b2545;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.route-stock-item .name{font-size:.78rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.route-stock-item .nums{display:flex;gap:.4rem;flex-wrap:wrap;margin-top:.55rem}
.route-stock-item .nums span{
  background:#f1f5f9;border-radius:999px;padding:.22rem .45rem;
  font-size:.74rem;font-weight:800;color:#334155
}
.route-stock-empty{
  background:#fff7ed;border:1px solid #fed7aa;color:#9a3412;
  border-radius:14px;padding:.8rem;font-weight:800
}
@media(max-width:991px){.route-stock-list{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:575px){.route-stock-list{grid-template-columns:1fr}.route-stock-totals span{width:100%;justify-content:space-between}}

.stock-pill{
  display:inline-flex;
  align-items:center;
  gap:5px;
  border-radius:999px;
  padding:4px 9px;
  font-size:.72rem;
  font-weight:950;
  letter-spacing:.02em;
  white-space:nowrap;
}
.stock-pill.stable,
.stock-pill.ok{
  background:#dcfce7;
  color:#166534;
}
.stock-pill.warning{
  background:#fef3c7;
  color:#92400e;
}
.stock-pill.danger{
  background:#fee2e2;
  color:#991b1b;
}
.stock-pill.processed{
  background:#e0f2fe;
  color:#075985;
}
.dispatch-item.stock-danger{
  background:linear-gradient(90deg,#fff1f2,#ffffff 48%);
}
.dispatch-item.stock-warning{
  background:linear-gradient(90deg,#fffbeb,#ffffff 48%);
}
.dispatch-item.stock-stable{
  background:linear-gradient(90deg,#f0fdf4,#ffffff 48%);
}
.dispatch-item.stock-danger.active{box-shadow:inset 4px 0 0 #dc2626;}
.dispatch-item.stock-warning.active{box-shadow:inset 4px 0 0 #f59e0b;}
.dispatch-item.stock-stable.active{box-shadow:inset 4px 0 0 #16a34a;}
.stock-advice{
  border-radius:16px;
  padding:11px 13px;
  margin-bottom:12px;
  font-size:.88rem;
  font-weight:800;
}
.stock-advice.stable,
.stock-advice.ok{background:#f0fdf4;border:1px solid #bbf7d0;color:#166534;}
.stock-advice.warning{background:#fffbeb;border:1px solid #fde68a;color:#92400e;}
.stock-advice.danger{background:#fff1f2;border:1px solid #fecdd3;color:#991b1b;}
.stock-advice.processed{background:#f0f9ff;border:1px solid #bae6fd;color:#075985;}
.stock-line{
  display:block;
  font-size:.74rem;
  margin-top:4px;
  font-weight:800;
}
.stock-line.stable,
.stock-line.ok{color:#166534;}
.stock-line.warning{color:#b45309;}
.stock-line.danger{color:#dc2626;}

</style>
</head>
<body>
  <nav class="navbar topbar navbar-expand-lg">
    <div class="container-fluid px-3 px-lg-4">
      <div>
        <div class="brand-title">BReport · Almacén Botella</div>
        <div class="brand-sub">Inventario de botellas, embarques, transferencias y control logístico por planta</div>
      </div>
      <div class="ms-auto d-flex align-items-center gap-2 flex-wrap">
        <span class="muted small"><?= h($u['full_name']) ?></span>
        <span class="scope-pill">
          <i class="bi bi-building"></i>
          <?= h($scopeCompanyName) ?> · <b><?= h($scopeLocationName) ?></b>
        </span>
        <select class="form-select form-select-sm scope-select" id="scopeLocationSel" title="Cambiar localidad"></select>
        <button class="btn nav-btn btn-sm" type="button" id="btnScopeChange">
          <i class="bi bi-arrow-repeat me-1"></i>Ver planta
        </button>
        <a class="btn nav-btn btn-sm" href="select_scope.php">
          <i class="bi bi-grid-1x2 me-1"></i>Empresa
        </a>
        <a class="btn nav-btn btn-sm" href="dashboard.php">Dashboard</a>
        <a class="btn nav-btn btn-sm" href="logout.php">Salir</a>
      </div>
    </div>
  </nav>

  <div class="page-wrap">
    <div class="shell">

      <div class="warehouse-command-center glass mb-4 top-priority">
        <div class="command-head">
          <div>
            <div class="command-eyebrow">Centro operativo de almacén</div>
            <div class="command-title">¿Qué operación necesitas realizar?</div>
            <div class="command-sub">Accesos rápidos para existencias, movimientos, rutas y control operativo. La bandeja de pedidos permanece visible debajo.</div>
          </div>
          <div class="command-live-pill">
            <i class="bi bi-clock-history"></i>
            <span>Actualizado con inventario oficial</span>
          </div>
        </div>

        <div class="warehouse-action-grid">
          <button type="button" class="warehouse-action-card" id="btnInventoryQuick">
            <span class="wa-icon wa-blue"><i class="bi bi-box-seam"></i></span>
            <span class="wa-content">
              <strong>Existencias</strong>
              <small>Saldo por artículo y almacén.</small>
            </span>
            <span class="wa-arrow"><i class="bi bi-chevron-right"></i></span>
          </button>

          <button type="button" class="warehouse-action-card" id="btnTransferQuick" <?= !$canWrite ? 'disabled' : '' ?>>
            <span class="wa-icon wa-purple"><i class="bi bi-arrow-left-right"></i></span>
            <span class="wa-content">
              <strong>Transferencias almacén</strong>
              <small>Mover producto entre almacenes físicos.</small>
            </span>
            <span class="wa-arrow"><i class="bi bi-chevron-right"></i></span>
          </button>

          <?php if ($canAddExistence): ?>
          <button type="button" class="warehouse-action-card" id="btnManualStockInQuick">
            <span class="wa-icon wa-green"><i class="bi bi-box-arrow-in-down"></i></span>
            <span class="wa-content">
              <strong>Agregar existencias</strong>
              <small>Entrada manual controlada a almacén.</small>
            </span>
            <span class="wa-arrow"><i class="bi bi-chevron-right"></i></span>
          </button>
          <?php endif; ?>

          <button type="button" class="warehouse-action-card" id="btnAddQuick" <?= !$canWrite ? 'disabled' : '' ?>>
            <span class="wa-icon wa-indigo"><i class="bi bi-truck"></i></span>
            <span class="wa-content">
              <strong>Registrar salidas</strong>
              <small>Salida manual desde almacén.</small>
            </span>
            <span class="wa-arrow"><i class="bi bi-chevron-right"></i></span>
          </button>

          <button type="button" class="warehouse-action-card operation-nav-btn" data-ops-section="returnSection">
            <span class="wa-icon wa-cyan"><i class="bi bi-arrow-counterclockwise"></i></span>
            <span class="wa-content">
              <strong>Registrar devoluciones</strong>
              <small>Consultar y registrar retornos.</small>
            </span>
            <span class="wa-arrow"><i class="bi bi-chevron-right"></i></span>
          </button>

          <button type="button" class="warehouse-action-card" id="btnMermaQuick" <?= !$canWrite ? 'disabled' : '' ?>>
            <span class="wa-icon wa-gold"><i class="bi bi-exclamation-triangle"></i></span>
            <span class="wa-content">
              <strong>Registrar merma de almacén</strong>
              <small>Descuento controlado por concepto.</small>
            </span>
            <span class="wa-arrow"><i class="bi bi-chevron-right"></i></span>
          </button>

          <button type="button" class="warehouse-action-card operation-nav-btn" data-ops-section="salesCreatedSection">
            <span class="wa-icon wa-orange"><i class="bi bi-signpost-split-fill"></i></span>
            <span class="wa-content">
              <strong>Rutas de reparto</strong>
              <small>Revisar órdenes cargadas por ruta.</small>
            </span>
            <span class="wa-count" id="countSalesCreated">0</span>
          </button>

          <a class="warehouse-action-card" href="revision_rutas_botella.php">
            <span class="wa-icon wa-cyan"><i class="bi bi-map-fill"></i></span>
            <span class="wa-content">
              <strong>Revisión de rutas</strong>
              <small>Existencias, movimientos y asignación de repartidor.</small>
            </span>
            <span class="wa-arrow"><i class="bi bi-box-arrow-up-right"></i></span>
          </a>

          <button type="button" class="warehouse-action-card operation-nav-btn active" data-ops-section="dispatchMailboxSection">
            <span class="wa-icon wa-dark"><i class="bi bi-inboxes-fill"></i></span>
            <span class="wa-content">
              <strong>Bandeja de pedidos</strong>
              <small>Órdenes de ventas y salidas pendientes.</small>
            </span>
            <span class="wa-arrow"><i class="bi bi-chevron-down"></i></span>
          </button>
        </div>
      </div>


      <div class="glass panel-pad mb-4">
        <div class="d-flex flex-wrap gap-3 align-items-start justify-content-between">
          <div>
            <div class="section-title mb-2">
              <i class="bi bi-box-seam-fill text-warning"></i>
              Almacén
            </div>
            <div class="muted">
              <?= h($scopeWarehouseHelpText) ?>
            </div>
          </div>

          <div class="warehouse-context-note">
            <i class="bi bi-info-circle"></i>
            <span>Usa el centro operativo superior para registrar movimientos. Esta sección conserva contexto, fecha operativa e indicadores internos.</span>
          </div>
        </div>

        <?php if ($isHieloBarraScope): ?>
        <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mt-3 mb-2">
          <button id="cardBar" class="btn btn-blue-soft btn-sm" type="button"><i class="bi bi-eye me-1"></i>Ver detalle de barra</button>
        </div>
        <?php endif; ?>

        <div class="history-filter-box d-flex flex-wrap align-items-end gap-2 mt-3 d-none" aria-hidden="true">
          <div>
            <label class="form-label small mb-1">Fecha inicial</label>
            <input class="form-control form-control-sm" type="date" id="histDateFrom">
          </div>
          <div>
            <label class="form-label small mb-1">Fecha final</label>
            <input class="form-control form-control-sm" type="date" id="histDateTo">
          </div>
          <button class="btn btn-blue-soft btn-sm" type="button" id="btnHistoryRefresh"><i class="bi bi-arrow-repeat me-1"></i>Actualizar</button>
          <button class="btn btn-green-soft btn-sm" type="button" id="btnExportMovements"><i class="bi bi-download me-1"></i>Descargar historial</button>
        </div>

        <div class="row g-2 mt-2 mb-1">
          <div class="col-12 col-lg-6">
            <div class="small-note d-flex flex-wrap gap-2 align-items-center justify-content-between">
              <span><i class="bi bi-clock-history me-1"></i>Hora de consulta</span>
              <span class="mono fw-bold" id="systemClockNow">Cargando...</span>
            </div>
          </div>
          <div class="col-12 col-lg-6">
            <div class="small-note d-flex flex-wrap gap-2 align-items-center justify-content-between">
              <span><i class="bi bi-calendar2-check me-1"></i>Fecha actual y turno detectado</span>
              <span class="mono fw-bold" id="systemClockOperational">Cargando...</span>
            </div>
          </div>
        </div>

        <div class="row g-3 mt-2 d-none" id="productCardsRow" aria-hidden="true"></div>
      </div>
      <div id="dispatchMailboxSection" class="module-section active">
        <div class="glass panel-pad mb-4">
          <div class="d-flex align-items-end justify-content-between gap-3 flex-wrap mb-3">
            <div>
              <div class="section-title mb-1">
                <i class="bi bi-inboxes-fill text-primary"></i>
                Bandeja operativa de salidas
              </div>
              <div class="small-note">
                <?php if ($isHieloBarraScope): ?>
                Vista completa por origen: VentasHielo por surtir, órdenes de Ventas y salidas manuales de Almacén. Aplica fecha solo cuando necesites conciliar un día.
                <?php else: ?>
                Vista logística Botella: órdenes autorizadas desde Ventas y salidas manuales de Almacén. VentasHielo no aplica para esta empresa. Aplica fecha solo cuando necesites conciliar un día.
                <?php endif; ?>
              </div>
            </div>
            <div class="d-flex align-items-end gap-2 flex-wrap">
              <div>
                <label class="form-label mb-1 small">Filtrar por día</label>
                <input id="dispatchMailboxDate" class="form-control" type="date">
                <div class="dispatch-filter-note">Vacío = mostrar todos</div>
              </div>
              <button id="btnDispatchToday" class="btn btn-blue-soft" type="button"><i class="bi bi-calendar-event me-1"></i>Hoy</button>
              <button id="btnDispatchAll" class="btn toolbar-btn" type="button"><i class="bi bi-calendar3 me-1"></i>Todos</button>
              <button id="btnDispatchReload" class="btn toolbar-btn" type="button"><i class="bi bi-arrow-repeat me-1"></i>Actualizar</button>
            </div>
          </div>

          <div class="dispatch-mailbox">
            <aside class="dispatch-folders">
              <?php if ($isHieloBarraScope): ?>
              <button class="dispatch-folder active" type="button" data-dispatch-folder="vh">
                <span><i class="bi bi-snow2 me-2"></i>VentasHielo</span>
                <span class="dispatch-folder-count" id="dispatchCountVH">0</span>
              </button>
              <?php endif; ?>
              <button class="dispatch-folder <?= $isHieloBarraScope ? '' : 'active' ?>" type="button" data-dispatch-folder="sales">
                <span><i class="bi bi-cart-check me-2"></i>Ventas</span>
                <span class="dispatch-folder-count" id="dispatchCountSales">0</span>
              </button>
              <button class="dispatch-folder" type="button" data-dispatch-folder="warehouse">
                <span><i class="bi bi-truck me-2"></i>Almacén</span>
                <span class="dispatch-folder-count" id="dispatchCountWarehouse">0</span>
              </button>

              <div class="dispatch-suggestion" id="dispatchSuggestion">
                Selecciona una carpeta para consultar salidas de la planta activa.
              </div>
            </aside>

            <section class="dispatch-list">
              <div class="dispatch-list-head">
                <div class="fw-bold" id="dispatchFolderTitle">Salidas</div>
                <div class="small muted" id="dispatchFolderSub">Consulta operativa por origen</div>
              </div>
              <div class="dispatch-items" id="dispatchMailboxItems"></div>
            </section>

            <section class="dispatch-detail" id="dispatchMailboxDetail">
              <div class="dispatch-empty">
                <i class="bi bi-envelope-open fs-1 mb-2"></i>
                <div class="fw-bold">Selecciona una salida</div>
                <div class="small">Aquí podrás revisar detalle y ejecutar las acciones permitidas.</div>
              </div>
            </section>
          </div>
        </div>
      </div>

      <div id="inventorySection" class="module-section d-none" aria-hidden="true">
      <div class="glass panel-pad mb-4 inventory-table-section">
        <div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
          <div class="section-title">
            <i class="bi bi-grid-1x2-fill text-warning"></i>
            Inventario disponible por producto
          </div>
          <div class="small-note pulse-soft">
            <?= h($scopeWarehouseHelpText) ?>
          </div>
        </div>

        <div class="table-wrap">
          <div class="table-responsive">
            <table class="table table-hover align-middle">
              <thead>
                <tr>
                  <th>Producto</th>
                  <th>Existencia por almacén</th>
                  <th class="text-end">Inicial</th>
                  <th class="text-end">Entradas</th>
                  <th class="text-end">Salidas</th>
                  <th class="text-end">Disponible total</th>
                  <th>UOM</th>
                </tr>
              </thead>
              <tbody id="invBody"></tbody>
            </table>
          </div>
        </div>
      </div>

      </div>
      </div>

      <?php if ($isHieloBarraScope): ?>
      <div id="vhSection" class="module-section">
      <div class="glass panel-pad mb-4">
        <div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
          <div class="section-title">
            <i class="bi bi-bag-check-fill text-warning"></i>
            Pedidos autorizados de VentasHielo
          </div>
          <div class="small-note">
            Aquí se muestran pedidos autorizada de VentasHielo listos para surtir desde almacén BReport.
          </div>
        </div>

        <div class="table-wrap">
          <div class="table-responsive">
            <table class="table table-hover align-middle">
              <thead>
                <tr>
                  <th>Folio</th>
                  <th>Fecha</th>
                  <th>Cliente</th>
                  <th>Items</th>
                  <th class="text-end">Piezas</th>
                  <th>Estatus</th>
                  <th class="text-end">Acciones</th>
                </tr>
              </thead>
              <tbody id="salesBody"></tbody>
            </table>
          </div>
        </div>
        <div class="muted small mt-3" id="salesEmptyHint"></div>
        <div id="salesPager" class="mt-2"></div>
      </div>
      </div>
      <?php endif; ?>

      <div id="salesCreatedSection" class="module-section">
      <div class="glass panel-pad mb-4">
        <div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
          <div>
            <div class="section-title mb-1">
              <i class="bi bi-signpost-split-fill text-warning"></i>
              Rutas de reparto con órdenes creadas desde ventas
            </div>
            <div class="small-note">
              Selecciona una fecha y revisa cada ruta activa. Rojo = pendiente de salida, verde = salida generada.
            </div>
          </div>
          <div class="route-filter-card d-flex align-items-end gap-2 flex-wrap">
            <div>
              <label class="form-label mb-1 small">Fecha de ruta</label>
              <input id="salesRouteDate" class="form-control" type="date">
            </div>
            <button id="btnLoadSalesRoutes" class="btn btn-blue-soft" type="button">
              <i class="bi bi-search me-1"></i>Consultar rutas
            </button>
          </div>
        </div>

        <div class="row g-3" id="salesRouteCards"></div>
        <div class="muted small mt-3" id="salesRouteEmptyHint"></div>
      </div>
      </div>


      <div id="locReqSection" class="module-section">
      <div class="glass panel-pad mb-4">
        <div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
          <div>
            <div class="section-title mb-1">
              <i class="bi bi-building-up text-warning"></i>
              Solicitudes entre localidades
            </div>
            <div class="small-note">
              Solo producto comercial: BARRA desde CF1/CF2 y bolsas desde CF3. MERMA, APROV-MERMA, CUB y SOB-CUB no se transfieren.
            </div>
          </div>
          <div class="d-flex gap-2 flex-wrap">
            <button id="btnLocReqReload" class="btn toolbar-btn" type="button"><i class="bi bi-arrow-repeat me-1"></i>Actualizar</button>
            <button id="btnLocReqNew" class="btn btn-green-soft" type="button" <?= !$canWrite ? 'disabled' : '' ?>><i class="bi bi-plus-lg me-1"></i>Nueva solicitud</button>
          </div>
        </div>

        <ul class="nav nav-pills gap-2 mb-3" id="locReqTabs">
          <li class="nav-item"><button class="nav-link active" type="button" data-locreq-mode="received">Recibidas para enviar</button></li>
          <li class="nav-item"><button class="nav-link" type="button" data-locreq-mode="created">Mis solicitudes</button></li>
          <li class="nav-item"><button class="nav-link" type="button" data-locreq-mode="transit">En tránsito</button></li>
          <li class="nav-item"><button class="nav-link" type="button" data-locreq-mode="history">Historial</button></li>
        </ul>

        <div class="table-wrap">
          <div class="table-responsive">
            <table class="table table-hover align-middle">
              <thead>
                <tr>
                  <th>Folio</th>
                  <th>Solicita</th>
                  <th>Origen proveedor</th>
                  <th>Usuario / autorización</th>
                  <th>Estado</th>
                  <th>Artículo / cantidad</th>
                  <th>Chofer / unidad</th>
                  <th>Fecha</th>
                  <th class="text-end">Acciones</th>
                </tr>
              </thead>
              <tbody id="locReqBody"></tbody>
            </table>
          </div>
        </div>
        <div class="muted small mt-3" id="locReqEmptyHint"></div>
      </div>
      </div>

      <div id="returnSection" class="module-section">
      <div class="glass panel-pad mb-4">
        <div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
          <div>
            <div class="section-title mb-1">
              <i class="bi bi-arrow-counterclockwise text-warning"></i>
              Devoluciones de almacén
            </div>
            <div class="small-note">
              Registra una entrada real al inventario cuando el cliente rechaza producto. La referencia de salida se captura manualmente.
            </div>
          </div>
          <button id="btnReturnSection" class="btn btn-blue-soft" type="button" <?= !$canWrite ? 'disabled' : '' ?>>
            <i class="bi bi-plus-lg me-1"></i>Nueva devolución
          </button>
        </div>

        <div class="small-note mb-3">
          La devolución no borra ni modifica la salida original. Genera un movimiento <b>IN</b> auditable en <b>inventory_moves</b> con <b>ref_type = WAREHOUSE_RETURN</b>.
        </div>

        <div class="table-wrap">
          <div class="table-responsive">
            <table class="table table-hover align-middle">
              <thead>
                <tr>
                  <th>Fecha</th>
                  <th>Producto</th>
                  <th>Almacén destino</th>
                  <th>Tipo</th>
                  <th>Referencia / motivo</th>
                  <th>Usuario</th>
                  <th class="text-end">Cantidad</th>
                </tr>
              </thead>
              <tbody id="returnBody"></tbody>
            </table>
          </div>
        </div>
        <div class="muted small mt-3" id="returnEmptyHint"></div>
      </div>
      </div>

    </div>
  </div>

  <div class="modal fade" id="editModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <form class="modal-content" id="editForm">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Crear orden logística Botella</div>
            <div class="muted small">Selecciona OSA para carga a ruta u OSV para salida urgente a cliente. La captura replica la lógica de Ventas Botella; el inventario se afecta cuando Almacén confirma la salida.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>

        <div class="modal-body">
          <div class="row g-3">
            <div class="col-12 col-md-4">
              <label class="form-label">Tipo de orden</label>
              <select class="form-select" name="order_type" id="fOrderType" required>
                <option value="RUTA">OSA · Ruta de reparto</option>
                <option value="CLIENTE">OSV · Salida a cliente</option>
              </select>
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Fecha</label>
              <input class="form-control" type="date" name="work_date" required>
            </div>
            <div class="col-12 col-md-4 js-route-field">
              <label class="form-label">Ruta de reparto</label>
              <select class="form-select" name="route_no" id="fRouteNo" required>
                <option value="">Cargando rutas...</option>
              </select>
              <input type="hidden" name="route_label" id="fRouteLabel">
            </div>
            <div class="col-12 col-md-4 d-none js-osv-field">
              <label class="form-label">OC cliente</label>
              <input class="form-control mono" name="reference" id="fReference" placeholder="OC cliente">
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Fecha / hora salida</label>
              <input class="form-control" type="datetime-local" name="depart_at">
            </div>

            <div class="col-12 col-md-4">
              <label class="form-label">Chofer</label>
              <input class="form-control" name="driver_name" placeholder="Nombre del chofer">
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Unidad</label>
              <input class="form-control" name="vehicle_unit" id="fVehicleUnit" placeholder="Camión, placa, unidad">
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Notas</label>
              <input class="form-control" name="notes" placeholder="Observaciones">
            </div>

            <div class="col-12 col-md-6 d-none js-osv-field">
              <label class="form-label">Cliente registrado</label>
              <select class="form-select" name="customer_id" id="fCustomer">
                <option value="">Selecciona cliente</option>
              </select>
            </div>
            <div class="col-12 col-md-6 d-none js-osv-field">
              <label class="form-label">Punto de entrega</label>
              <select class="form-select" name="address_id" id="fAddressId">
                <option value="">Selecciona cliente</option>
              </select>
            </div>
            <div class="col-12 d-none js-osv-field">
              <label class="form-label">Dirección seleccionada</label>
              <input class="form-control" name="delivery_address" id="fDeliveryAddress" readonly>
            </div>

            <div class="col-12 col-md-4">
              <label class="form-label">Fecha de entrega</label>
              <input class="form-control" type="date" name="delivery_date">
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Hora de entrega</label>
              <input class="form-control" type="time" name="delivery_time">
            </div>
            <div class="col-12 col-md-4 d-flex align-items-end">
              <div class="capacity-mini w-100" id="routeLoadTotalBox">
                <div class="small muted">Carga capturada</div>
                <div class="fw-bold mono"><span id="routeLoadPallets">0</span> tarimas</div>
              </div>
            </div>
          </div>

          <input type="hidden" name="channel_id" id="fChannelHidden" value="0">
          <input type="hidden" name="customer_name" id="fCustomerName">
          <input type="hidden" name="customer_code" id="fCustomerCode">
          <input type="hidden" name="customer_business_name" id="fCustomerBusinessName">
          <input type="hidden" name="customer_location_label" id="fCustomerLocationLabel">

          <div class="route-stock-panel js-route-field d-none" id="routeStockPanel">
            <div class="d-flex justify-content-between align-items-start gap-2 flex-wrap">
              <div>
                <div class="fw-bold"><i class="bi bi-truck text-primary me-1"></i>Existencia actual de la ruta</div>
                <div class="small muted" id="routeStockSubtitle">Selecciona una ruta para revisar producto cargado actualmente.</div>
              </div>
              <div class="route-stock-totals">
                <span><b id="routeStockPieces">0</b> pz</span>
                <span><b id="routeStockPackages">0</b> paq</span>
                <span><b id="routeStockPallets">0</b> tar</span>
              </div>
            </div>
            <div class="route-stock-list mt-3" id="routeStockList"></div>
            <div class="small-note mt-3">
              Si capturas un artículo que ya existe en la ruta, la nueva carga se sumará al saldo actual cuando Montacargas/Almacén confirme la salida.
            </div>
          </div>

          <hr class="my-4">
          <div class="d-flex justify-content-between align-items-center gap-2 flex-wrap mb-3">
            <div>
              <div class="fw-bold"><i class="bi bi-box-seam text-primary me-1"></i>Artículos a cargar</div>
              <div class="small muted">Captura producto, embalaje y tarimas. Esta pantalla solo planea la carga; inventario se afecta al confirmar la salida.</div>
            </div>
            <button type="button" class="btn btn-primary-soft btn-sm" id="btnAddManualOutItem">
              <i class="bi bi-plus-circle me-1"></i>Agregar artículo
            </button>
          </div>
          <div id="itemsWrap" class="route-items-wrap"></div>
          <div class="alert alert-warning d-none mt-3" id="routeItemsAlert"></div>
        </div>

        <div class="modal-footer">
          <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cancelar</button>
          <button class="btn btn-green-soft" type="submit" id="btnSaveManualOut">
            <i class="bi bi-save me-1"></i>Crear orden
          </button>
        </div>
      </form>
    </div>
  </div>

  <div class="modal fade" id="transferModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
      <form class="modal-content" id="transferForm">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Transferencia entre almacenes</div>
            <div class="muted small">Mover producto de un almacén a otro dentro de la misma planta.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>

        <div class="modal-body">
          <div class="row g-3">
            <div class="col-12 col-md-6">
              <label class="form-label">Fecha</label>
              <input class="form-control" type="date" name="work_date" required>
            </div>

            <div class="col-12 col-md-6">
              <label class="form-label">Producto</label>
              <select class="form-select" name="product_id" id="tProduct" required></select>
            </div>

            <div class="col-12 col-md-6">
              <label class="form-label">Almacén origen</label>
              <select class="form-select" name="from_warehouse_id" id="tFromWarehouse" required></select>
            </div>

            <div class="col-12 col-md-6">
              <label class="form-label">Almacén destino</label>
              <select class="form-select" name="to_warehouse_id" id="tToWarehouse" required></select>
            </div>

            <div class="col-12 col-md-6">
              <label class="form-label">Cantidad</label>
              <input class="form-control mono" name="qty" inputmode="decimal" value="0" required>
            </div>

            <div class="col-12">
              <label class="form-label">Notas</label>
              <input class="form-control" name="notes" placeholder="Ej. Reabasto de barra para cuarto cubicado">
            </div>
          </div>
        </div>

        <div class="modal-footer">
          <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cancelar</button>
          <button class="btn btn-blue-soft" type="submit">
            <i class="bi bi-arrow-left-right me-1"></i>Guardar transferencia
          </button>
        </div>
      </form>
    </div>
  </div>

  <div class="modal fade" id="mermaModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
      <form class="modal-content" id="mermaForm">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Registrar merma de almacén</div>
            <div class="muted small">Descuenta del almacén seleccionado y genera registro de calidad.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>

        <div class="modal-body">
          <div class="row g-3">
            <div class="col-12 col-md-6">
              <label class="form-label">Fecha</label>
              <input class="form-control" type="date" name="work_date" required>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Concepto de merma</label>
              <select class="form-select" name="concept_id" id="mConcept" required></select>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Producto</label>
              <select class="form-select" name="product_id" id="mProduct" required></select>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Almacén</label>
              <select class="form-select" name="warehouse_id" id="mWarehouse" required></select>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Cantidad</label>
              <input class="form-control mono" name="qty" inputmode="decimal" value="0" required>
              <div class="small mt-2 muted" id="mWarehouseStockHint">Disponible en almacén: 0</div>
            </div>
            <div class="col-12">
              <label class="form-label">Notas</label>
              <input class="form-control" name="notes" placeholder="Motivo o detalle de la merma">
            </div>
          </div>
        </div>

        <div class="modal-footer">
          <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cancelar</button>
          <button class="btn btn-gold-soft" type="submit">
            <i class="bi bi-save me-1"></i>Guardar merma
          </button>
        </div>
      </form>
    </div>
  </div>

  <?php if ($canAddExistence): ?>
  <div class="modal fade" id="manualStockInModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
      <form class="modal-content" id="manualStockInForm">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Agregar existencias</div>
            <div class="muted small">Formato de recargas/producción de Botella: racks/tarimas, paquetes y piezas.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-info mb-3">
            <i class="bi bi-info-circle me-2"></i>
            Esta operación generará una entrada real <b>IN</b> en <b>inventory_moves</b>.
            No está disponible para Hielo Barra.
          </div>
          <div class="row g-3">
            <div class="col-12 col-md-3">
              <label class="form-label">Fecha operativa</label>
              <input class="form-control mono" id="msiWorkDate" type="date" readonly>
              <div class="small muted mt-1">Se respeta el corte operativo de las 07:00 AM.</div>
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Línea</label>
              <input class="form-control" id="msiLineName" maxlength="60" placeholder="Ej. S">
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Proveedor</label>
              <input class="form-control" id="msiProvider" maxlength="80" placeholder="Ej. Sid / recargas">
            </div>

            <div class="col-12 col-md-3">
              <label class="form-label">Ruta</label>
              <select class="form-select" id="msiRoute">
                <option value="">Sin ruta / interno</option>
              </select>
            </div>

            <div class="col-12 col-md-6">
              <label class="form-label">Artículo / presentación</label>
              <select class="form-select" id="msiProduct" required></select>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Almacén destino</label>
              <select class="form-select" id="msiWarehouse" required></select>
              <div class="small muted mt-1" id="msiWarehouseHint">Selecciona artículo para consultar almacenes.</div>
            </div>

            <div class="col-12 col-md-6">
              <label class="form-label">Embalaje</label>
              <select class="form-select" id="msiPackaging" required></select>
              <div class="small muted mt-1">Define paquetes/rack y piezas/paquete.</div>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Cálculo automático</label>
              <input class="form-control mono" id="msiCalcPreview" value="Total paquetes / total piezas" readonly>
            </div>

            <div class="col-6 col-md-3">
              <label class="form-label"># Rack's / tarimas</label>
              <input class="form-control mono" id="msiRackQty" type="number" min="0" step="1" inputmode="numeric" value="0">
            </div>
            <div class="col-6 col-md-3">
              <label class="form-label">Paquetes sueltos</label>
              <input class="form-control mono" id="msiLoosePackages" type="number" min="0" step="1" inputmode="numeric" value="0">
            </div>
            <div class="col-6 col-md-3">
              <label class="form-label">Piezas sueltas</label>
              <input class="form-control mono" id="msiLoosePieces" type="number" min="0" step="1" inputmode="numeric" value="0">
            </div>
            <div class="col-6 col-md-3">
              <label class="form-label">Total piezas IN</label>
              <input class="form-control mono" id="msiTotalPieces" value="0" readonly>
            </div>

            <div class="col-12">
              <label class="form-label">Nota / observación</label>
              <textarea class="form-control" id="msiNotes" rows="2" maxlength="160" placeholder="Opcional: referencia interna o comentario de captura."></textarea>
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cancelar</button>
          <button class="btn btn-green-soft" type="submit">
            <i class="bi bi-eye me-1"></i>Revisar movimiento
          </button>
        </div>
      </form>
    </div>
  </div>

  <div class="modal fade" id="manualStockInConfirmModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Confirmar entrada de existencias</div>
            <div class="muted small">Revisa la cantidad antes y después de aplicar el movimiento.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-warning mb-3">
            <i class="bi bi-exclamation-triangle me-2"></i>
            Al confirmar se registrará una entrada real en inventario con referencia <b>MANUAL_STOCK_IN</b>.
          </div>
          <div id="manualStockInConfirmSummary"></div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Regresar</button>
          <button id="btnConfirmManualStockIn" class="btn btn-green-soft" type="button">
            <i class="bi bi-check2-circle me-1"></i>Confirmar entrada
          </button>
        </div>
      </div>
    </div>
  </div>

  <div class="modal fade" id="manualStockInResultModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Existencia agregada</div>
            <div class="muted small">Movimiento real registrado correctamente.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-success mb-3">
            <i class="bi bi-check2-circle me-2"></i>
            La cantidad fue agregada al almacén seleccionado y ya forma parte del inventario actual.
          </div>
          <div id="manualStockInResultSummary"></div>
        </div>
        <div class="modal-footer">
          <button class="btn btn-blue-soft" type="button" data-bs-dismiss="modal">Cerrar</button>
        </div>
      </div>
    </div>
  </div>
  <?php endif; ?>

  <div class="modal fade" id="returnModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
      <form class="modal-content" id="returnForm">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Registrar devolución de almacén</div>
            <div class="muted small">Entrada real por producto rechazado. Captura manualmente la salida original.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>

        <div class="modal-body">
          <div class="alert alert-info mb-3">
            <i class="bi bi-info-circle me-2"></i>
            Esta devolución genera un movimiento <b>IN</b> real en inventario. No borra ni modifica la salida original.
          </div>

          <div class="row g-3">
            <div class="col-12 col-md-6">
              <label class="form-label">Fecha devolución</label>
              <input class="form-control" type="date" name="work_date" required>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Referencia / número de salida original</label>
              <input class="form-control" name="reference" placeholder="Ej. OUT-123, OSV-0001, PW-0001" required>
            </div>

            <div class="col-12 col-md-6">
              <label class="form-label">Producto devuelto</label>
              <select class="form-select" name="product_id" id="rProduct" required></select>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Almacén destino</label>
              <select class="form-select" name="warehouse_id" id="rWarehouse" required></select>
              <div class="small mt-2 muted" id="rWarehouseStockHint">Disponible actual: 0</div>
            </div>

            <div class="col-12 col-md-6">
              <label class="form-label">Cantidad devuelta</label>
              <input class="form-control mono" name="qty" inputmode="decimal" value="0" required>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Motivo</label>
              <select class="form-select" name="reason" required>
                <option value="Rechazo cliente">Rechazo cliente</option>
                <option value="Producto retornado en buen estado">Producto retornado en buen estado</option>
                <option value="Error de surtido">Error de surtido</option>
                <option value="Cancelación de entrega">Cancelación de entrega</option>
                <option value="Otro">Otro</option>
              </select>
            </div>
            <div class="col-12">
              <label class="form-label">Notas</label>
              <input class="form-control" name="notes" placeholder="Detalle adicional de la devolución">
            </div>
          </div>
        </div>

        <div class="modal-footer">
          <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cancelar</button>
          <button class="btn btn-blue-soft" type="submit">
            <i class="bi bi-save me-1"></i>Guardar devolución
          </button>
        </div>
      </form>
    </div>
  </div>

  <div class="modal fade" id="returnConfirmModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Confirmar devolución de almacén</div>
            <div class="muted small">Valida el impacto antes de registrar la entrada real al inventario.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-warning mb-3">
            <i class="bi bi-exclamation-triangle me-2"></i>
            Al confirmar se generará un movimiento real <b>IN</b>. La salida original permanece intacta.
          </div>
          <div id="returnConfirmSummary"></div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Regresar</button>
          <button id="btnConfirmWarehouseReturn" class="btn btn-blue-soft" type="button">
            <i class="bi bi-check2-circle me-1"></i>Confirmar devolución
          </button>
        </div>
      </div>
    </div>
  </div>

  <div class="modal fade" id="returnResultModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Devolución registrada</div>
            <div class="muted small">Entrada real de inventario generada correctamente.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-success mb-3">
            <i class="bi bi-check2-circle me-2"></i>
            El almacén destino fue actualizado mediante <b>WAREHOUSE_RETURN</b>.
          </div>
          <div id="returnResultSummary"></div>
        </div>
        <div class="modal-footer">
          <button class="btn btn-blue-soft" type="button" data-bs-dismiss="modal">Cerrar</button>
        </div>
      </div>
    </div>
  </div>

  <div class="modal fade" id="detailModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Detalle Barra / Producto terminado</div>
            <div class="muted small">Consulta rápida de barra por almacén y producto terminado.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>

        <div class="modal-body">
          <div class="row g-3 mb-3">
            <div class="col-12 col-md-6">
              <div class="kpi-card p-3">
                <div class="kpi-label">Barras usadas en cubicado</div>
                <div class="kpi-value mono text-warning" id="barsUsedKpi">0</div>
                <div class="kpi-hint">Dato informativo de proceso</div>
              </div>
            </div>
            <div class="col-12 col-md-6">
              <div class="small-note h-100 d-flex align-items-center" id="detailNote"></div>
            </div>
          </div>

          <div class="section-title mb-3">
            <i class="bi bi-snow text-warning"></i>
            Barra comercial por almacén (CF1 + CF2)
          </div>
          <div class="table-wrap mb-4">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>Almacén</th>
                    <th class="text-end">Cantidad</th>
                  </tr>
                </thead>
                <tbody id="barsWhBody"></tbody>
              </table>
            </div>
          </div>

          <div class="section-title mb-3">
            <i class="bi bi-exclamation-triangle text-warning"></i>
            Barra en inventario interno / segregado
          </div>
          <div class="table-wrap mb-4">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>Almacén</th>
                    <th class="text-end">Cantidad</th>
                  </tr>
                </thead>
                <tbody id="barsInternalBody"></tbody>
              </table>
            </div>
          </div>

          <div class="section-title mb-3">
            <i class="bi bi-box-fill text-warning"></i>
            Productos terminados
          </div>
          <div class="table-wrap">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>Producto</th>
                    <th class="text-end">Cantidad</th>
                    <th>UOM</th>
                  </tr>
                </thead>
                <tbody id="ptBody"></tbody>
              </table>
            </div>
          </div>
        </div>

        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cerrar</button>
        </div>
      </div>
    </div>
  </div>


  <div class="modal fade" id="routeOrdersModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5" id="routeOrdersTitle">Ruta</div>
            <div class="muted small" id="routeOrdersSub">Pedidos de la ruta seleccionada.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="row g-3" id="routeOrdersList"></div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cerrar</button>
        </div>
      </div>
    </div>
  </div>

  <div class="modal fade" id="salesDetailModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Pedido autorizado para surtir</div>
            <div class="muted small">Revisión de pedido listo para generar salida de almacén.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>

        <div class="modal-body">
          <div class="row g-3 mb-4" id="salesDetailHeader"></div>

          <div class="section-title mb-3">
            <i class="bi bi-box-seam text-warning"></i>
            Artículos del pedido
          </div>
          <div class="table-wrap mb-4">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>SKU</th>
                    <th>Producto</th>
                    <th>Embalaje</th>
                    <th class="text-end">Paquetes</th>
                    <th class="text-end">Tarimas</th>
                    <th class="text-end">Botellas</th>
                  </tr>
                </thead>
                <tbody id="salesDetailItems"></tbody>
              </table>
            </div>
          </div>

          <div class="glass panel-pad">
            <div class="section-title mb-3">
              <i class="bi bi-truck text-warning"></i>
              Datos para generar salida de almacén
            </div>

            <div class="row g-3">
              <input type="hidden" id="sdOrderId">

              <div class="col-12 col-md-4">
                <label class="form-label">Fecha</label>
                <input id="sdWorkDate" class="form-control" type="date">
              </div>

              <div class="col-12 col-md-4">
                <label class="form-label">Canal</label>
                <select id="sdChannelId" class="form-select"></select>
              </div>

              <div class="col-12 col-md-4">
                <label class="form-label">Referencia</label>
                <input id="sdReference" class="form-control" placeholder="Folio pedido / embarque">
              </div>

              <div class="col-12 col-md-4">
                <label class="form-label">Cliente / destino</label>
                <input id="sdCustomer" class="form-control" placeholder="Cliente / destino">
              </div>

              <div class="col-12 col-md-4 d-none">
                <label class="form-label">Chofer</label>
                <input id="sdDriverName" class="form-control" placeholder="Chofer no requerido">
              </div>

              <div class="col-12 col-md-4">
                <label class="form-label">Número de transporte confirmado</label>
                <input id="sdVehicleUnit" class="form-control" placeholder="Se toma de Montacargas" readonly>
                <div class="form-text">Se carga automáticamente desde la orden enviada a Montacargas.</div>
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">Hora salida</label>
                <input id="sdDepartAt" class="form-control" type="datetime-local">
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">Notas</label>
                <input id="sdNotes" class="form-control" placeholder="Observaciones">
              </div>
            </div>
          </div>
        </div>

        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cerrar</button>
          <button id="btnSalesTruckPlan" class="btn btn-primary-soft" type="button">
            <i class="bi bi-truck-front me-1"></i>Carga camión / Montacargas
          </button>
          <button id="btnDispatchSalesOrder" class="btn btn-gold-soft" type="button">
            <i class="bi bi-box-arrow-up-right me-1"></i>Generar salida y PDF
          </button>
          <button id="btnUploadInvoice" class="btn btn-green-soft d-none" type="button">
            <i class="bi bi-receipt-cutoff me-1"></i>Adjuntar factura
          </button>
          <a id="btnSalesOrderPdf" class="btn btn-green-soft d-none" href="#" target="_blank" rel="noopener">
            <i class="bi bi-file-earmark-pdf me-1"></i>PDF carta
          </a>
          <a id="btnSalesOrderTicketPdf" class="btn btn-blue-soft d-none" href="#" target="_blank" rel="noopener">
            <i class="bi bi-receipt me-1"></i>Ticket 80mm
          </a>
        </div>
      </div>
    </div>
  </div>

  <div class="modal fade" id="botellaPalletAlertModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Salida detenida · revisar tarimas</div>
            <div class="muted small">No se afectó inventario.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-warning mb-3">
            <i class="bi bi-exclamation-triangle me-2"></i>
            Botella solo permite surtir tarimas completas.
          </div>
          <div id="botellaPalletAlertText" class="fw-semibold"></div>
        </div>
        <div class="modal-footer">
          <button class="btn btn-blue-soft" type="button" data-bs-dismiss="modal">Entendido, revisar orden</button>
        </div>
      </div>
    </div>
  </div>


  <div class="modal fade" id="botellaTruckPlanModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Carga camión · detalle para Almacén</div>
            <div class="muted small">Clientes, OC, direcciones, tarimas y distribución visual generada desde Ventas.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div id="botellaTruckPlanStatus" class="mb-3"></div>
          <div class="table-wrap mb-3">
            <table class="table table-sm align-middle mb-0">
              <thead><tr><th>OC</th><th>Cliente</th><th>Dirección</th><th>Artículo</th><th class="text-end">Tarimas</th><th class="text-end">Paquetes</th><th class="text-end">Piezas</th><th>Acomodo</th></tr></thead>
              <tbody id="botellaTruckPlanBody"></tbody>
            </table>
          </div>
          <div class="glass panel-pad">
            <div class="section-title mb-3"><i class="bi bi-person-badge text-warning"></i> Crear orden de carga a montacargas</div>
            <div class="row g-3">
              <input type="hidden" id="flOrderId">
              <div class="col-12 col-md-6"><label class="form-label">Número de transporte a cargar</label><input id="flVehicleUnit" class="form-control" placeholder="Ej. Caja seca 53 / Unidad 12 / CLIO-01"></div>
              <div class="col-12 col-md-6"><label class="form-label">Notas</label><input id="flNotes" class="form-control" placeholder="Observaciones para montacargas"></div>
              <input type="hidden" id="flDriverName" value="">
            </div>
            <div class="small muted mt-2">La salida queda bloqueada hasta que el montacarguista confirme carga completa. El número de transporte se reutilizará automáticamente al generar la salida.</div>
          </div>
        </div>
        <div class="modal-footer">
          <a id="btnBotellaTruckPrint" class="btn btn-primary-soft" href="#" target="_blank" rel="noopener"><i class="bi bi-printer me-1"></i>Ver / imprimir camión</a>
          <button id="btnCreateForkliftOrder" class="btn btn-gold-soft" type="button"><i class="bi bi-send-check me-1"></i>Enviar orden a montacargas</button>
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cerrar</button>
        </div>
      </div>
    </div>
  </div>

  <div class="modal fade" id="confirmDispatchModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Confirmar salida de almacén</div>
            <div class="muted small">Revisa la información antes de afectar inventario.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-warning mb-3">
            <i class="bi bi-exclamation-triangle me-2"></i>
            ¿Deseas confirmar la salida de almacén para este pedido?
          </div>
          <div id="confirmDispatchSummary" class="small"></div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cancelar</button>
          <button id="btnConfirmDispatchYes" class="btn btn-gold-soft" type="button">
            <i class="bi bi-check2-circle me-1"></i>Sí, generar salida
          </button>
        </div>
      </div>
    </div>
  </div>

  <div class="modal fade" id="dispatchResultModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Salida generada</div>
            <div class="muted small">Proceso terminado correctamente.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-success mb-3">
            <i class="bi bi-check2-circle me-2"></i>
            Ya se realizó la salida de almacén.
          </div>
          <div id="dispatchResultSummary" class="small"></div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cerrar</button>
          <a id="btnDownloadDispatchPdf" class="btn btn-gold-soft" href="#" target="_blank" rel="noopener">
            <i class="bi bi-file-earmark-pdf me-1"></i>PDF carta
          </a>
          <a id="btnDownloadDispatchTicketPdf" class="btn btn-blue-soft" href="#" target="_blank" rel="noopener">
            <i class="bi bi-receipt me-1"></i>Ticket 80mm
          </a>
        </div>
      </div>
    </div>
  </div>




  <div class="modal fade" id="invoiceUploadModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
      <form class="modal-content" id="invoiceUploadForm" enctype="multipart/form-data">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Adjuntar factura</div>
            <div class="muted small">Al guardar la factura, la solicitud cambia a ENTREGADA.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <input type="hidden" id="invoiceOrderId" name="order_id" value="0">
          <div class="mb-3">
            <label class="form-label">Número de factura</label>
            <input class="form-control" id="invoiceNumber" name="invoice_number" maxlength="80" placeholder="Ej. F-12345" required>
          </div>
          <div class="mb-2">
            <label class="form-label">Archivo de factura</label>
            <input class="form-control" id="invoiceFile" name="invoice_file" type="file" accept=".pdf,.xml,.jpg,.jpeg,.png,.webp" required>
            <div class="form-text">Formatos permitidos: PDF, XML, JPG, JPEG, PNG o WEBP. Máximo 10 MB.</div>
          </div>
          <div id="invoiceUploadMsg" class="small mt-2"></div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cancelar</button>
          <button class="btn btn-green-soft" type="submit"><i class="bi bi-upload me-1"></i>Guardar factura y entregar</button>
        </div>
      </form>
    </div>
  </div>

  <div class="modal fade" id="locReqModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <form class="modal-content" id="locReqForm">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Nueva solicitud entre localidades</div>
            <div class="muted small">Solicita producto comercial a otra localidad de la misma empresa.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-info py-2">
            Este flujo no mueve inventario hasta que la localidad proveedora confirma el envío. No permite MERMA, APROV-MERMA, CUB ni SOB-CUB.
          </div>
          <div class="row g-3">
            <div class="col-12 col-md-4">
              <label class="form-label">Localidad proveedora</label>
              <select class="form-select" name="source_location_id" id="lrSourceLocation" required></select>
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Fecha requerida</label>
              <input class="form-control" type="date" name="required_date">
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Notas</label>
              <input class="form-control" name="notes" placeholder="Motivo / comentario">
            </div>
          </div>
          <hr>
          <div class="d-flex justify-content-between align-items-center mb-2">
            <div class="section-title"><i class="bi bi-box-seam text-warning"></i> Artículos solicitados</div>
            <button class="btn toolbar-btn" type="button" id="btnLocReqAddItem"><i class="bi bi-plus-lg me-1"></i>Agregar artículo</button>
          </div>
          <div id="locReqItemsWrap" class="d-grid gap-3"></div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cancelar</button>
          <button class="btn btn-green-soft" type="submit"><i class="bi bi-send me-1"></i>Enviar solicitud</button>
        </div>
      </form>
    </div>
  </div>

  <div class="modal fade" id="locReqDetailModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5" id="locReqDetailTitle">Solicitud</div>
            <div class="muted small" id="locReqDetailSub"></div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body" id="locReqDetailBody"></div>
        <div class="modal-footer" id="locReqDetailFooter">
          <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button>
        </div>
      </div>
    </div>
  </div>


  <div class="modal fade" id="locReqConfirmModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5" id="locReqConfirmTitle">Confirmar</div>
            <div class="muted small" id="locReqConfirmSub">Valida la información antes de continuar.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body" id="locReqConfirmBody"></div>
        <div class="modal-footer">
          <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cancelar</button>
          <button type="button" class="btn btn-blue-soft" id="locReqConfirmOk"><i class="bi bi-check2-circle me-1"></i>Confirmar</button>
        </div>
      </div>
    </div>
  </div>

  <div class="modal fade" id="locReqRejectModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
      <form class="modal-content" id="locReqRejectForm">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Rechazar solicitud</div>
            <div class="muted small">El motivo es obligatorio y quedará visible en el historial.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <input type="hidden" id="locReqRejectId" value="0">
          <label class="form-label">Motivo del rechazo</label>
          <textarea class="form-control" id="locReqRejectReason" rows="4" maxlength="255" placeholder="Indica por qué se rechaza la solicitud" required></textarea>
          <div class="form-text">No podrás finalizar el rechazo sin capturar el motivo.</div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cancelar</button>
          <button type="submit" class="btn btn-outline-danger"><i class="bi bi-x-circle me-1"></i>Rechazar solicitud</button>
        </div>
      </form>
    </div>
  </div>


  <div class="modal fade" id="movementHistoryModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5" id="movementHistoryTitle">Historial de movimientos</div>
            <div class="muted small" id="movementHistorySub">Consulta por producto, fecha y tipo.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="history-filter-box mb-3">
            <div class="row g-2 align-items-end">
              <div class="col-12 col-md-3">
                <label class="form-label small mb-1">Fecha inicial</label>
                <input class="form-control form-control-sm" type="date" id="mhDateFrom">
              </div>
              <div class="col-12 col-md-3">
                <label class="form-label small mb-1">Fecha final</label>
                <input class="form-control form-control-sm" type="date" id="mhDateTo">
              </div>
              <div class="col-12 col-md-3">
                <label class="form-label small mb-1">Tipo</label>
                <select class="form-select form-select-sm" id="mhDirection">
                  <option value="ALL">Todos</option>
                  <option value="IN">Entradas</option>
                  <option value="OUT">Salidas</option>
                </select>
              </div>
              <div class="col-12 col-md-3">
                <button class="btn btn-blue-soft btn-sm w-100" type="button" id="btnMovementHistorySearch"><i class="bi bi-search me-1"></i>Consultar</button>
              </div>
            </div>
          </div>
          <div class="table-wrap">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>Fecha / hora</th>
                    <th>Tipo</th>
                    <th>Almacén</th>
                    <th>Origen</th>
                    <th>Usuario</th>
                    <th class="text-end">Cantidad</th>
                    <th>Notas</th>
                  </tr>
                </thead>
                <tbody id="movementHistoryBody"></tbody>
              </table>
            </div>
          </div>
          <div class="d-flex justify-content-between align-items-center gap-2 flex-wrap mt-3" id="movementHistoryPager"></div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cerrar</button>
        </div>
      </div>
    </div>
  </div>


  <div class="modal fade" id="inventoryQuickModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-box-seam me-1"></i>Existencias de almacén</div>
            <div class="muted small">Inventario oficial calculado desde inventory_moves. Haz clic en una tarjeta para ver historial del producto.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="history-filter-box mb-3">
            <div class="row g-2 align-items-end">
              <div class="col-12 col-md-3">
                <label class="form-label small mb-1">Fecha inicial</label>
                <input class="form-control form-control-sm" type="date" id="invModalDateFrom">
              </div>
              <div class="col-12 col-md-3">
                <label class="form-label small mb-1">Fecha final</label>
                <input class="form-control form-control-sm" type="date" id="invModalDateTo">
              </div>
              <div class="col-12 col-md-3">
                <button class="btn btn-blue-soft btn-sm w-100" type="button" id="btnInventoryModalRefresh"><i class="bi bi-arrow-repeat me-1"></i>Actualizar existencias</button>
              </div>
              <div class="col-12 col-md-3">
                <button class="btn btn-green-soft btn-sm w-100" type="button" id="btnInventoryModalExport"><i class="bi bi-download me-1"></i>Descargar historial</button>
              </div>
            </div>
          </div>

          <div class="inventory-modal-summary mb-3">
            <div class="inventory-modal-kpi">
              <span>Productos con saldo</span>
              <strong id="invModalProducts">0</strong>
            </div>
            <div class="inventory-modal-kpi">
              <span>Entradas del rango</span>
              <strong id="invModalIn">0</strong>
            </div>
            <div class="inventory-modal-kpi">
              <span>Salidas del rango</span>
              <strong id="invModalOut">0</strong>
            </div>
            <div class="inventory-modal-kpi">
              <span>Disponible actual</span>
              <strong id="invModalCurrent">0</strong>
            </div>
          </div>

          <div class="row g-3 mb-3" id="inventoryModalCards"></div>

          <div class="table-wrap">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>Producto</th>
                    <th>Existencia por almacén</th>
                    <th class="text-end">Inicial</th>
                    <th class="text-end">Entradas</th>
                    <th class="text-end">Salidas</th>
                    <th class="text-end">Disponible total</th>
                    <th>UOM</th>
                  </tr>
                </thead>
                <tbody id="inventoryModalBody"></tbody>
              </table>
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cerrar</button>
        </div>
      </div>
    </div>
  </div>


  <button id="pendingWatchBtn" class="pending-watch-float" type="button" title="Pedidos por surtir · actualización manual">
    <span class="watch-icon"><i class="bi bi-bell-fill"></i></span>
    <span class="watch-main">
      <span class="watch-count"><span id="pendingWatchCount">0</span> por surtir</span>
      <span class="watch-timer" id="pendingWatchTimer" aria-hidden="true"></span>
    </span>
  </button>
  <div id="pendingWatchToast" class="pending-watch-toast" role="status" aria-live="polite">
    <div class="toast-title"><i class="bi bi-bell-fill text-primary me-1"></i>Nuevos pedidos para revisar</div>
    <div class="toast-body" id="pendingWatchToastBody">Hay pedidos pendientes de surtir.</div>
  </div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const CSRF = <?= json_encode($csrf) ?>;
const CAN_WRITE = <?= json_encode($canWrite) ?>;
const CURRENT_COMPANY_ID = <?= json_encode($companyId) ?>;
const CURRENT_LOCATION_ID = <?= json_encode($locationId) ?>;
const CURRENT_COMPANY_NAME = <?= json_encode($scopeCompanyName) ?>;
const CURRENT_LOCATION_NAME = <?= json_encode($scopeLocationName) ?>;
const IS_HIELO_BARRA_SCOPE = <?= json_encode($isHieloBarraScope) ?>;
const CAN_ADD_EXISTENCE = <?= json_encode($canAddExistence) ?>;

const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, m => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));
const num = (v) => {
  const n = parseFloat(v);
  return isNaN(n) ? 0 : n;
};
// Cantidades comerciales: se muestran enteras para evitar ruido visual (0.000, 77.000, etc.).
// La base de datos conserva su precisión DECIMAL; aquí solo redondeamos la presentación.
const fmt = (v) => String(Math.round(num(v)));
const fmtMoney = (v) => num(v).toFixed(2);
const fmtBar = (v) => String(Math.round(num(v)));

async function apiGet(action, params = {}){
  const u = new URL(location.href);
  u.search = '';
  u.searchParams.set('action', action);
  for (const [k,v] of Object.entries(params)) {
    if (v !== '' && v !== null && v !== undefined) {
      u.searchParams.set(k, v);
    }
  }

  const r = await fetch(u.toString(), {
    credentials:'same-origin',
    headers:{'X-CSRF': CSRF}
  });
  const txt = await r.text();
  try {
    const data = JSON.parse(txt);
    if (data && typeof data === 'object' && !('http_status' in data)) data.http_status = r.status;
    return data;
  } catch(e){
    const clean = String(txt || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
    console.warn('Respuesta no JSON', {action, status:r.status, preview:clean.slice(0,180)});
    return {
      ok:false,
      http_status:r.status,
      error: r.status === 403
        ? 'El servidor bloqueó esta consulta. Se omitió para no detener la pantalla.'
        : (r.status === 404 ? 'Endpoint no encontrado.' : 'Respuesta no válida del servidor.'),
      raw_preview: clean.slice(0,180)
    };
  }
}

async function apiPost(action, payload = {}){
  const u = new URL(location.href);
  u.search = '';
  u.searchParams.set('action', action);

  const r = await fetch(u.toString(), {
    method:'POST',
    credentials:'same-origin',
    headers:{
      'Content-Type':'application/json',
      'X-CSRF':CSRF
    },
    body: JSON.stringify(payload)
  });
  const txt = await r.text();
  try {
    const data = JSON.parse(txt);
    if (data && typeof data === 'object' && !('http_status' in data)) data.http_status = r.status;
    return data;
  } catch(e){
    const clean = String(txt || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
    console.warn('Respuesta no JSON', {action, status:r.status, preview:clean.slice(0,180)});
    return {
      ok:false,
      http_status:r.status,
      error: r.status === 403
        ? 'El servidor bloqueó esta operación. Revisa permisos/reglas del hosting.'
        : (r.status === 404 ? 'Endpoint no encontrado.' : 'Respuesta no válida del servidor.'),
      raw_preview: clean.slice(0,180)
    };
  }
}


const PENDING_WATCH_INTERVAL_MS = 5 * 60 * 1000;
let pendingWatchLastCount = null;
let pendingWatchLastOrderId = 0;
let pendingWatchNextAt = Date.now() + PENDING_WATCH_INTERVAL_MS;
let pendingWatchTimerHandle = null;
let pendingWatchCheckHandle = null;

function pendingWatchFormatRemaining(ms){
  const total = Math.max(0, Math.ceil(ms / 1000));
  const m = String(Math.floor(total / 60)).padStart(2,'0');
  const s = String(total % 60).padStart(2,'0');
  return `${m}:${s}`;
}
function pendingWatchSetCount(count){
  if ($('pendingWatchCount')) $('pendingWatchCount').textContent = String(count || 0);
}
function pendingWatchShowToast(message){
  const toast = $('pendingWatchToast');
  const body = $('pendingWatchToastBody');
  if (!toast || !body) return;
  body.innerHTML = message;
  toast.classList.add('show');
  window.clearTimeout(toast._hideTimer);
  toast._hideTimer = window.setTimeout(() => toast.classList.remove('show'), 6500);
}
function pendingWatchUpdateTimer(){
  const remaining = pendingWatchFormatRemaining(pendingWatchNextAt - Date.now());
  const btn = $('pendingWatchBtn');
  if (btn) btn.title = 'Pedidos por surtir · próxima revisión en ' + remaining;
  if ($('pendingWatchTimer')) $('pendingWatchTimer').textContent = '';
}
async function checkPendingDispatchWatch(manual = false){
  try{
    const j = await apiGet('dispatch_pending_counter');
    if (!j.ok) return;
    const count = parseInt(j.pending_count || 0, 10) || 0;
    const lastId = parseInt(j.last_order_id || 0, 10) || 0;
    const previousCount = pendingWatchLastCount;
    const previousLastId = pendingWatchLastOrderId;
    pendingWatchSetCount(count);

    const hasNew = previousCount !== null && (count > previousCount || lastId > previousLastId);
    if (hasNew) {
      $('pendingWatchBtn')?.classList.add('has-new');
      const sample = (j.rows || []).slice(0, 3).map(r => `<div class="mono">${esc(r.sales_order_ref || '')} · ${esc(r.business_name || r.contact_name || '')}</div>`).join('');
      pendingWatchShowToast(`<b>${count}</b> pedido(s) por surtir detectados.${sample ? '<div class="mt-2">'+sample+'</div>' : ''}`);
      if (typeof loadDispatchMailbox === 'function') await loadDispatchMailbox();
    } else if (manual) {
      pendingWatchShowToast(count > 0 ? `<b>${count}</b> pedido(s) por surtir en bandeja.` : 'No hay pedidos por surtir pendientes.');
    }

    pendingWatchLastCount = count;
    pendingWatchLastOrderId = lastId;
    pendingWatchNextAt = Date.now() + PENDING_WATCH_INTERVAL_MS;
    pendingWatchUpdateTimer();
  } catch(e){
    console.warn('No se pudo validar pedidos por surtir', e);
  }
}
function initPendingDispatchWatch(){
  // CORRECCIÓN BOTELLA:
  // El endpoint action=dispatch_pending_counter fue bloqueado por reglas del servidor/hosting (403 Forbidden).
  // Se desactiva la consulta automática de 5 minutos para evitar que la página quede inestable o parezca bloqueada.
  // La bandeja se actualiza con el botón Actualizar y al entrar a la sección, sin afectar OSV/OSA ni inventario.
  if (pendingWatchTimerHandle) window.clearInterval(pendingWatchTimerHandle);
  if (pendingWatchCheckHandle) window.clearInterval(pendingWatchCheckHandle);
  pendingWatchTimerHandle = null;
  pendingWatchCheckHandle = null;

  const btn = $('pendingWatchBtn');
  if (!btn) return;
  btn.title = 'Pedidos por surtir · actualización manual';
  btn.classList.remove('has-new');
  btn.addEventListener('click', async () => {
    if (typeof showModuleSection === 'function') showModuleSection('dispatch');
    if (typeof loadDispatchMailbox === 'function') await loadDispatchMailbox();
  });
}

let modal, detailModal, transferModal, returnModal, returnConfirmModal, returnResultModal, salesDetailModal, routeOrdersModal, confirmDispatchModal, dispatchResultModal, invoiceUploadModal, locReqModal, locReqDetailModal, locReqConfirmModal, locReqRejectModal, movementHistoryModal, inventoryQuickModal, botellaPalletAlertModal, botellaTruckPlanModal;
let manualStockInModal, manualStockInConfirmModal, manualStockInResultModal;
let pendingWarehouseReturnPayload = null;
let pendingManualStockInPayload = null;
let products = [];
let channels = [];
let warehouses = [];
let manualOutCustomers = [];
let manualOutAddresses = [];
let manualOutRoutes = [];
let manualRouteStockRows = [];
let manualRouteStockMap = new Map();
let invRows = [];
let barProductId = 0;
let salesRows = [];
let salesCreatedRows = [];
let salesRouteGroups = [];
let selectedRouteGroup = null;
let salesPage = 1;
let salesPageSize = 10;
let salesPages = 1;
let salesTotal = 0;
let currentSalesOrder = null;
let mermaRows = [];
let mermaPage = 1;
let mermaPageSize = 10;
let mermaPagination = {};
let locReqRows = [];
let locReqMode = 'received';
let locReqLocations = [];
let historySummaryRows = [];
let movementHistoryState = {product_id:0, sku:'', product_name:'', direction:'ALL', date_from:'', date_to:'', page:1, per_page:25};
let systemTimeInfo = null;
let operationalAutoRefreshRunning = false;

let dispatchMailboxFolder = IS_HIELO_BARRA_SCOPE ? 'vh' : 'sales';
let dispatchMailboxAllDates = true;
let dispatchMailboxRows = {vh: [], sales: [], warehouse: []};
let dispatchMailboxRowsAll = {vh: [], sales: [], warehouse: []};
let dispatchShowProcessedSales = false;
let dispatchMailboxSelected = null;

function dispatchIsProcessedSalesOrder(row){
  const status = String(row?.status || '').toUpperCase();
  return !!row?.outbound_id || status !== 'AUTORIZADA';
}

function applyDispatchSalesProcessedFilter(){
  const allSales = dispatchMailboxRowsAll.sales || [];
  dispatchMailboxRows.sales = dispatchShowProcessedSales
    ? allSales
    : allSales.filter(r => !dispatchIsProcessedSalesOrder(r));
}


/**
 * Diálogos BReport
 * Reemplaza alert/confirm nativos por modales Bootstrap para mantener
 * una experiencia consistente dentro del sistema.
 */
function ensureAppDialogModal(){
  let el = document.getElementById('appDialogModal');
  if (!el) {
    document.body.insertAdjacentHTML('beforeend', `
      <div class="modal fade" id="appDialogModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
        <div class="modal-dialog modal-dialog-centered">
          <div class="modal-content soft-card overflow-hidden">
            <div class="modal-header" id="appDialogHeader">
              <div>
                <h5 class="modal-title" id="appDialogTitle">Aviso</h5>
                <div class="small" id="appDialogSub">BReport · Almacén Botella</div>
              </div>
              <button type="button" class="btn-close btn-close-white" id="appDialogCloseX" aria-label="Cerrar"></button>
            </div>
            <div class="modal-body">
              <div id="appDialogBody" class="fs-6"></div>
            </div>
            <div class="modal-footer" id="appDialogFooter">
              <button type="button" class="btn toolbar-btn" id="appDialogCancel">Cancelar</button>
              <button type="button" class="btn btn-blue-soft" id="appDialogOk">Aceptar</button>
            </div>
          </div>
        </div>
      </div>
    `);
  }
  return bootstrap.Modal.getOrCreateInstance(document.getElementById('appDialogModal'));
}

function appDialogHtml(message){
  const raw = String(message ?? '');
  if (raw.includes('<') && raw.includes('>')) return raw;
  return `<div style="white-space:pre-line">${esc(raw)}</div>`;
}

function appAlert(message, options = {}){
  return new Promise(resolve => {
    const modalInstance = ensureAppDialogModal();
    const el = document.getElementById('appDialogModal');
    const title = document.getElementById('appDialogTitle');
    const sub = document.getElementById('appDialogSub');
    const body = document.getElementById('appDialogBody');
    const ok = document.getElementById('appDialogOk');
    const cancel = document.getElementById('appDialogCancel');
    const closeX = document.getElementById('appDialogCloseX');

    title.textContent = options.title || 'Aviso';
    sub.textContent = options.sub || 'BReport · Almacén Botella';
    body.innerHTML = appDialogHtml(message);
    ok.textContent = options.okText || 'Aceptar';
    ok.className = options.okClass || 'btn btn-blue-soft';
    cancel.classList.add('d-none');

    const done = () => {
      ok.onclick = null;
      closeX.onclick = null;
      modalInstance.hide();
      resolve(true);
    };
    ok.onclick = done;
    closeX.onclick = done;

    el.addEventListener('hidden.bs.modal', () => resolve(true), {once:true});
    modalInstance.show();
  });
}

function appConfirm(message, options = {}){
  return new Promise(resolve => {
    const modalInstance = ensureAppDialogModal();
    const el = document.getElementById('appDialogModal');
    const title = document.getElementById('appDialogTitle');
    const sub = document.getElementById('appDialogSub');
    const body = document.getElementById('appDialogBody');
    const ok = document.getElementById('appDialogOk');
    const cancel = document.getElementById('appDialogCancel');
    const closeX = document.getElementById('appDialogCloseX');

    let settled = false;
    title.textContent = options.title || 'Confirmar';
    sub.textContent = options.sub || 'BReport · Almacén Botella';
    body.innerHTML = appDialogHtml(message);
    ok.textContent = options.okText || 'Confirmar';
    ok.className = options.okClass || 'btn btn-blue-soft';
    cancel.textContent = options.cancelText || 'Cancelar';
    cancel.className = options.cancelClass || 'btn toolbar-btn';
    cancel.classList.remove('d-none');

    const finish = (value) => {
      if (settled) return;
      settled = true;
      ok.onclick = null;
      cancel.onclick = null;
      closeX.onclick = null;
      modalInstance.hide();
      resolve(value);
    };
    ok.onclick = () => finish(true);
    cancel.onclick = () => finish(false);
    closeX.onclick = () => finish(false);
    el.addEventListener('hidden.bs.modal', () => finish(false), {once:true});
    modalInstance.show();
  });
}

// Bloquea el uso de alert nativo. Cualquier aviso existente se transforma en modal.
window.alert = function(message){ appAlert(message); };



async function loadScopeSelector(){
  const sel = $('scopeLocationSel');
  if (!sel) return;
  const j = await apiGet('scope_locations');
  if (!j.ok) {
    sel.innerHTML = `<option value="${esc(CURRENT_LOCATION_ID)}">${esc(CURRENT_LOCATION_NAME)}</option>`;
    return;
  }

  const rows = j.rows || [];
  sel.innerHTML = rows.map(r => {
    const label = `${r.name || ('Localidad #' + r.id)}${r.code ? ' · ' + r.code : ''}`;
    return `<option value="${esc(r.id)}" ${Number(r.id) === Number(j.current_location_id) ? 'selected' : ''}>${esc(label)}</option>`;
  }).join('');

  if (!sel.innerHTML.trim()) {
    sel.innerHTML = `<option value="${esc(CURRENT_LOCATION_ID)}">${esc(CURRENT_LOCATION_NAME)}</option>`;
  }
}

async function changeScopeLocation(){
  const sel = $('scopeLocationSel');
  if (!sel) return;
  const newLocationId = Number(sel.value || 0);
  if (!newLocationId || newLocationId === Number(CURRENT_LOCATION_ID)) {
    await reloadAll();
    return;
  }

  const selectedName = sel.options[sel.selectedIndex]?.textContent || ('Localidad #' + newLocationId);
  const ok = await appConfirm(`Cambiar vista de almacén a:\n\n${CURRENT_COMPANY_NAME} · ${selectedName}\n\nSe recargará el módulo con el inventario de esa planta.`, {title:'Cambiar planta', okText:'Cambiar planta'});
  if (!ok) {
    sel.value = String(CURRENT_LOCATION_ID);
    return;
  }

  const btn = $('btnScopeChange');
  const prev = btn ? btn.innerHTML : '';
  if (btn) {
    btn.disabled = true;
    btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Cambiando';
  }

  try {
    const j = await apiPost('scope_change', {location_id: newLocationId});
    if (!j.ok) {
      alert(j.error || 'No se pudo cambiar de planta.');
      if (sel) sel.value = String(CURRENT_LOCATION_ID);
      return;
    }
    window.location.reload();
  } finally {
    if (btn) {
      btn.disabled = false;
      btn.innerHTML = prev;
    }
  }
}

function operationalDateForForms(){
  return systemTimeInfo?.operational_today || getMonthRange().from;
}

function renderSystemClock(){
  if (!systemTimeInfo) return;
  if ($('systemClockNow')) {
    $('systemClockNow').textContent = `${systemTimeInfo.php_now || ''} · ${systemTimeInfo.timezone_php || ''}`;
  }
  if ($('systemClockOperational')) {
    $('systemClockOperational').textContent = `${systemTimeInfo.operational_today || ''} · Turno ${systemTimeInfo.operational_shift || '-'}`;
  }
}


async function apiUpload(action, formData){
  const u = new URL(location.href);
  u.search = '';
  u.searchParams.set('action', action);
  const r = await fetch(u.toString(), {
    method:'POST',
    credentials:'same-origin',
    headers:{'X-CSRF':CSRF},
    body: formData
  });
  const txt = await r.text();
  try {
    const data = JSON.parse(txt);
    if (data && typeof data === 'object' && !('http_status' in data)) data.http_status = r.status;
    return data;
  } catch(e){
    const clean = String(txt || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
    console.warn('Respuesta no JSON', {action, status:r.status, preview:clean.slice(0,180)});
    return {ok:false, http_status:r.status, error: r.status === 403 ? 'El servidor bloqueó la carga del archivo.' : 'Respuesta no válida del servidor.', raw_preview: clean.slice(0,180)};
  }
}

async function loadSystemClock(){
  const previousOperationalDate = systemTimeInfo?.operational_today || null;
  const previousWasCurrentView = previousOperationalDate
    && $('histDateFrom') && $('histDateTo')
    && $('histDateFrom').value === previousOperationalDate
    && $('histDateTo').value === previousOperationalDate;

  const j = await apiGet('time_debug');
  if (j.ok) {
    systemTimeInfo = j;
    renderSystemClock();

    /*
     * Si el módulo permaneció abierto durante el corte de las 07:00 AM
     * y el operador estaba consultando el día operativo actual, renovamos
     * automáticamente el rango al nuevo día. No se alteran consultas históricas.
     */
    const changedOperationalDay = previousOperationalDate
      && previousOperationalDate !== j.operational_today;

    if (changedOperationalDay && previousWasCurrentView && !operationalAutoRefreshRunning) {
      operationalAutoRefreshRunning = true;
      try {
        $('histDateFrom').value = j.operational_today;
        $('histDateTo').value = j.operational_today;
        if ($('salesRouteDate')) $('salesRouteDate').value = j.operational_today;
        await loadInventoryForSelectedDates();
        await loadMovementSummary();
        renderProductCards();
        await loadWarehouseMermas(1);
      } finally {
        operationalAutoRefreshRunning = false;
      }
    }
  } else {
    if ($('systemClockNow')) $('systemClockNow').textContent = 'No disponible';
    if ($('systemClockOperational')) $('systemClockOperational').textContent = 'No disponible';
  }
}

function isNonCommercialAprovMerma(row){
  const txt = String((row?.sku || '') + ' ' + (row?.name || '')).toUpperCase().trim();
  return txt.includes('APROVECHAMIENTO') && txt.includes('MERMA');
}

function renderProductCards(){
  const wrap = $('productCardsRow');
  if (!wrap) return;

  const moveMap = {};
  (historySummaryRows || []).forEach(r => { moveMap[Number(r.product_id)] = r; });

  const sorted = [...invRows]
    .filter(x => !isNonCommercialAprovMerma(x))
    .filter(x => Math.abs(num(x.on_hand)) > 0.0001 || Math.abs(num(x.inventory_initial)) > 0.0001 || moveMap[Number(x.product_id)])
    .sort((a, b) => String(a.sku).localeCompare(String(b.sku), 'es'));

  if (!sorted.length) {
    wrap.innerHTML = `
      <div class="col-12">
        <div class="item-row">
          <div class="muted">Sin movimientos de inventario para mostrar en el rango seleccionado.</div>
        </div>
      </div>
    `;
    return;
  }

  wrap.innerHTML = sorted.map(r => {
    const isBar = IS_HIELO_BARRA_SCOPE && String(r.sku || '').toUpperCase() === 'BARRA 150 KILOS';
    const m = moveMap[Number(r.product_id)] || {};
    const qtyInitial = num(r.inventory_initial);
    const qtyIn = num(m.qty_in);
    const qtyOut = num(m.qty_out);
    const current = num(r.on_hand);
    const fmtQty = (v) => fmt(v);
    const expected = qtyInitial + qtyIn - qtyOut;
    const diff = current - expected;
    const diffWarn = Math.abs(diff) > 0.01;

    return `
      <div class="inventory-product-col col-12 col-md-6 col-xl-4">
        <div class="product-mini-card p-3 ${isBar ? 'product-mini-bar' : ''}" data-inventory-product="${esc(r.product_id)}" data-inventory-sku="${esc(r.sku || '')}">
          <div class="d-flex align-items-start justify-content-between gap-2">
            <div class="flex-grow-1">
              <div class="product-mini-title">${esc(r.sku)}</div>
              <div class="product-mini-sub">${esc(r.name || '')}</div>
            </div>
            ${isBar ? `<span class="badge-soft"><i class="bi bi-snow"></i>BARRA</span>` : ``}
          </div>

          <div class="product-move-grid">
            <div class="product-move-stat">
              <div class="product-move-label">inicial</div>
              <div class="product-move-value initial mono">${fmtQty(qtyInitial)}</div>
            </div>
            <div class="product-move-stat">
              <div class="product-move-label">Entradas</div>
              <div class="product-move-value in mono">${fmtQty(qtyIn)}</div>
            </div>
            <div class="product-move-stat">
              <div class="product-move-label">Salidas</div>
              <div class="product-move-value out mono">${fmtQty(qtyOut)}</div>
            </div>
            <div class="product-move-stat">
              <div class="product-move-label">Actual</div>
              <div class="product-move-value current mono">${fmtQty(current)}</div>
            </div>
          </div>
          <div class="product-mini-sub mt-2">${esc(r.uom || 'pz')} · clic para movimientos${diffWarn ? ` · revisar diferencia ${fmtQty(diff)}` : ``}</div>
          <div class="product-warehouse-list">${
            (Array.isArray(r.warehouse_breakdown) && r.warehouse_breakdown.length)
              ? r.warehouse_breakdown.map(w => `
                  <span class="product-warehouse-pill ${num(w.qty) < 0 ? 'neg' : ''}">
                    <b>${esc(w.code || '')}</b>
                    <span class="mono">${fmt(num(w.qty))}</span>
                  </span>
                `).join('')
              : '<span class="product-mini-sub">Sin almacén con existencia</span>'
          }</div>
        </div>
      </div>
    `;
  }).join('');

  wrap.querySelectorAll('[data-inventory-product]').forEach(card => {
    card.addEventListener('click', () => openMovementHistory(card.dataset.inventoryProduct, 'ALL', 1));
  });
}

function renderInventoryModalContent(){
  const cardSource = $('productCardsRow');
  const cardTarget = $('inventoryModalCards');
  const bodyTarget = $('inventoryModalBody');
  const invSource = $('invBody');

  if (cardTarget) {
    cardTarget.innerHTML = cardSource && cardSource.innerHTML.trim()
      ? cardSource.innerHTML
      : '<div class="col-12"><div class="inventory-modal-empty"><i class="bi bi-box-seam me-1"></i>Sin existencias para mostrar en el rango seleccionado.</div></div>';
    cardTarget.querySelectorAll('[data-inventory-product]').forEach(card => {
      card.addEventListener('click', () => openMovementHistory(card.dataset.inventoryProduct, 'ALL', 1));
    });
  }

  if (bodyTarget) {
    bodyTarget.innerHTML = invSource && invSource.innerHTML.trim()
      ? invSource.innerHTML
      : '<tr><td colspan="7" class="text-center text-muted py-4">Sin inventario para mostrar.</td></tr>';
    bodyTarget.querySelectorAll('[data-history-product]').forEach(btn => {
      btn.addEventListener('click', () => openMovementHistory(btn.dataset.historyProduct, btn.dataset.historyDirection || 'ALL', 1));
    });
  }

  const visibleRows = (invRows || []).filter(x => !isNonCommercialAprovMerma(x));
  const moveMap = {};
  (historySummaryRows || []).forEach(r => { moveMap[Number(r.product_id)] = r; });
  const productsWithStock = visibleRows.filter(x => Math.abs(num(x.on_hand)) > 0.0001).length;
  const totalCurrent = visibleRows.reduce((a, x) => a + num(x.on_hand), 0);
  const totalIn = Object.values(moveMap).reduce((a, x) => a + num(x.qty_in), 0);
  const totalOut = Object.values(moveMap).reduce((a, x) => a + num(x.qty_out), 0);

  if ($('invModalProducts')) $('invModalProducts').textContent = fmt(productsWithStock);
  if ($('invModalIn')) $('invModalIn').textContent = fmt(totalIn);
  if ($('invModalOut')) $('invModalOut').textContent = fmt(totalOut);
  if ($('invModalCurrent')) $('invModalCurrent').textContent = fmt(totalCurrent);
}

async function openInventoryQuickModal(){
  if ($('invModalDateFrom')) $('invModalDateFrom').value = $('histDateFrom')?.value || getMonthRange().from;
  if ($('invModalDateTo')) $('invModalDateTo').value = $('histDateTo')?.value || getMonthRange().to;
  renderInventoryModalContent();
  if (inventoryQuickModal) inventoryQuickModal.show();
}

async function refreshInventoryQuickModal(){
  if ($('histDateFrom') && $('invModalDateFrom')) $('histDateFrom').value = $('invModalDateFrom').value || getMonthRange().from;
  if ($('histDateTo') && $('invModalDateTo')) $('histDateTo').value = $('invModalDateTo').value || getMonthRange().to;
  await loadInventoryForSelectedDates();
  await loadMovementSummary();
  renderProductCards();
  renderInventoryModalContent();
}

function exportInventoryModalHistory(){
  const dateFrom = $('invModalDateFrom')?.value || $('histDateFrom')?.value || getMonthRange().from;
  const dateTo = $('invModalDateTo')?.value || $('histDateTo')?.value || getMonthRange().to;
  window.location.href = `?action=moves_export_csv&date_from=${encodeURIComponent(dateFrom)}&date_to=${encodeURIComponent(dateTo)}&direction=ALL`;
}

function renderInventory(){
  const tb = $('invBody');
  tb.innerHTML = '';

  const sorted = [...invRows]
    .filter(x => !isNonCommercialAprovMerma(x))
    .filter(x => Math.abs(num(x.on_hand)) > 0.0001 || Math.abs(num(x.manual_in)) > 0.0001 || Math.abs(num(x.manual_out)) > 0.0001 || Math.abs(num(x.inventory_initial)) > 0.0001 || (x.warehouse_breakdown || []).length)
    .sort((a,b) => String(a.sku).localeCompare(String(b.sku), 'es'));

  for (const r of sorted) {
    const isBar = IS_HIELO_BARRA_SCOPE && String(r.sku).toUpperCase() === 'BARRA 150 KILOS';
    const available = num(r.on_hand);
    const warehouses = Array.isArray(r.warehouse_breakdown) ? r.warehouse_breakdown : [];
    const whHtml = warehouses.length
      ? `<div class="warehouse-chip-wrap">${
          warehouses.map(w => `
            <span class="warehouse-chip ${num(w.qty) < 0 ? 'neg' : ''}">
              <b>${esc(w.code || '')}</b> ${fmt(num(w.qty))}
            </span>
          `).join('')
        }</div>`
      : `<span class="small muted">Sin existencia por almacén</span>`;

    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td>
          <div class="fw-semibold">${esc(r.sku)}</div>
          <div class="small">${esc(r.name)}</div>
          ${isBar ? `<div class="small text-primary fw-semibold mt-1"><i class="bi bi-snow me-1"></i>Materia prima Hielo Barra</div>` : ``}
        </td>
        <td>${whHtml}</td>
        <td class="text-end mono">${fmt(num(r.inventory_initial))}</td>
        <td class="text-end mono text-success fw-semibold">${fmt(num(r.manual_in))}</td>
        <td class="text-end mono text-danger fw-semibold">${fmt(num(r.manual_out))}</td>
        <td class="text-end mono fw-bold ${available < 0 ? 'text-danger' : ''}" style="color:${available < 0 ? '#c63b50' : '#2563eb'};">
          ${fmt(available)}
        </td>
        <td class="mono">${esc(r.uom || '')}</td>
      </tr>
    `);
  }

  if (!sorted.length) {
    tb.innerHTML = `<tr><td colspan="7" class="text-center muted py-4">Sin datos de inventario para la planta seleccionada.</td></tr>`;
  }
}

function getMonthRange(){
  // Rango operativo por defecto: solo la fecha operativa actual.
  // En BReport el día operativo inicia a las 07:00 AM Reynosa.
  // Antes de las 07:00 AM, el sistema debe seguir trabajando el día anterior.
  if (systemTimeInfo?.operational_today) {
    return {from: systemTimeInfo.operational_today, to: systemTimeInfo.operational_today};
  }
  const now = new Date();
  const y = now.getFullYear();
  const m = String(now.getMonth()+1).padStart(2,'0');
  const d = String(now.getDate()).padStart(2,'0');
  const today = `${y}-${m}-${d}`;
  return {from: today, to: today};
}

function initHistoryDates(){
  const range = getMonthRange();
  if ($('histDateFrom') && !$('histDateFrom').value) $('histDateFrom').value = range.from;
  if ($('histDateTo') && !$('histDateTo').value) $('histDateTo').value = range.to;
}

async function loadMovementSummary(){
  initHistoryDates();
  const dateFrom = $('histDateFrom')?.value || getMonthRange().from;
  const dateTo = $('histDateTo')?.value || getMonthRange().to;
  const j = await apiGet('moves_product_summary', {date_from: dateFrom, date_to: dateTo});
  if (!j.ok) {
    if ($('historySummaryHint')) $('historySummaryHint').textContent = j.error || 'No se pudo cargar el histórico.';
    return;
  }
  historySummaryRows = j.rows || [];
  renderMovementSummaryCards();
}

function renderMovementSummaryCards(){
  const rows = historySummaryRows || [];
  renderMovementSummaryTab('historyInCards', 'historyInHint', rows.filter(r => num(r.qty_in) > 0.0001), 'IN');
  renderMovementSummaryTab('historyOutCards', 'historyOutHint', rows.filter(r => num(r.qty_out) > 0.0001), 'OUT');
}

function renderMovementSummaryTab(containerId, hintId, rows, direction){
  const wrap = $(containerId);
  if (!wrap) return;
  const isIn = direction === 'IN';
  if (!rows.length) {
    wrap.innerHTML = `<div class="col-12"><div class="item-row text-center muted py-4">Sin ${isIn ? 'entradas' : 'salidas'} en el rango seleccionado.</div></div>`;
    if ($(hintId)) $(hintId).textContent = '';
    return;
  }
  wrap.innerHTML = rows.map(r => {
    const qty = isIn ? num(r.qty_in) : num(r.qty_out);
    const isBar = IS_HIELO_BARRA_SCOPE && String(r.sku || '').toUpperCase() === 'BARRA 150 KILOS';
    const displayQty = fmt(qty);
    return `
      <div class="col-12 col-md-6 col-xl-4">
        <div class="history-summary-card" data-move-product="${esc(r.product_id)}" data-move-direction="${direction}">
          <div class="d-flex justify-content-between align-items-start gap-2">
            <div>
              <div class="history-summary-title">${esc(r.sku || '')}</div>
              <div class="history-summary-sub">${esc(r.product_name || '')}</div>
            </div>
            <span class="history-summary-pill ${isIn ? 'in' : 'out'}">
              <i class="bi ${isIn ? 'bi-arrow-down-circle' : 'bi-arrow-up-circle'}"></i>${isIn ? 'Entradas' : 'Salidas'}
            </span>
          </div>
          <div class="history-summary-value ${isIn ? 'in' : 'out'} mono mt-3">${displayQty}</div>
          <div class="history-summary-sub mt-2">${esc(r.uom || 'pz')} · clic para detalle</div>
        </div>
      </div>
    `;
  }).join('');
  if ($(hintId)) $(hintId).textContent = `${isIn ? 'Entradas' : 'Salidas'} con movimiento: ${rows.length}`;
}

async function openMovementHistory(productId, direction='ALL', page=1){
  const row = (historySummaryRows || []).find(x => Number(x.product_id) === Number(productId)) || {};
  movementHistoryState = {
    product_id: Number(productId),
    sku: row.sku || '',
    product_name: row.product_name || '',
    direction: String(direction || 'ALL').toUpperCase(),
    date_from: $('histDateFrom')?.value || getMonthRange().from,
    date_to: $('histDateTo')?.value || getMonthRange().to,
    page: page || 1,
    per_page: 25
  };
  if ($('mhDateFrom')) $('mhDateFrom').value = movementHistoryState.date_from;
  if ($('mhDateTo')) $('mhDateTo').value = movementHistoryState.date_to;
  if ($('mhDirection')) $('mhDirection').value = movementHistoryState.direction;
  if ($('movementHistoryTitle')) $('movementHistoryTitle').textContent = `${movementHistoryState.direction === 'IN' ? 'Entradas' : movementHistoryState.direction === 'OUT' ? 'Salidas' : 'Movimientos'} · ${movementHistoryState.sku}`;
  if ($('movementHistorySub')) $('movementHistorySub').textContent = movementHistoryState.product_name || 'Historial de movimientos por producto';
  await loadMovementHistoryPage(page || 1);
  movementHistoryModal.show();
}

async function loadMovementHistoryPage(page=1){
  movementHistoryState.page = page;
  movementHistoryState.date_from = $('mhDateFrom')?.value || movementHistoryState.date_from;
  movementHistoryState.date_to = $('mhDateTo')?.value || movementHistoryState.date_to;
  movementHistoryState.direction = $('mhDirection')?.value || movementHistoryState.direction;
  const j = await apiGet('moves_product_detail', movementHistoryState);
  if (!j.ok) {
    alert(j.error || 'No se pudo cargar el historial del producto.');
    return;
  }
  renderMovementHistoryRows(j.rows || [], j.pagination || {});
}

function renderMovementHistoryRows(rows, pagination){
  const tb = $('movementHistoryBody');
  if (!tb) return;
  if (!rows.length) {
    tb.innerHTML = `<tr><td colspan="7" class="text-center muted py-4">Sin movimientos en el rango seleccionado.</td></tr>`;
  } else {
    tb.innerHTML = rows.map(r => {
      const type = String(r.move_type || '').toUpperCase();
      const badge = type === 'OUT'
        ? '<span class="badge text-bg-danger">Salida</span>'
        : (type === 'ADJ' ? '<span class="badge text-bg-primary">Ajuste / entrada</span>' : '<span class="badge text-bg-success">Entrada</span>');
      return `
        <tr>
          <td><div class="mono fw-semibold">${esc(r.work_date || '')}</div><div class="small muted mono">${esc(String(r.created_at || '').substring(11,19))}</div></td>
          <td>${badge}</td>
          <td><div class="mono mh-warehouse-code">${esc(r.warehouse_code || '-')}</div><div class="small muted">${esc(r.warehouse_name || '')}</div></td>
          <td><div class="mono">${esc(r.ref_type || r.channel_name || '-')}</div><div class="small muted">${r.ref_id ? '#' + esc(r.ref_id) : ''}</div></td>
          <td><div class="small fw-semibold">${esc(r.user_name || r.username || '-')}</div><div class="small muted mono">${esc(r.username || '')}</div></td>
          <td class="text-end mono fw-bold ${type === 'OUT' ? 'text-danger' : 'text-success'}">${fmt(r.qty)}</td>
          <td class="small">${esc(r.notes || '')}</td>
        </tr>
      `;
    }).join('');
  }
  const pg = $('movementHistoryPager');
  if (pg) {
    const page = Number(pagination.page || 1);
    const totalPages = Number(pagination.total_pages || 1);
    const totalRows = Number(pagination.total_rows || 0);
    pg.innerHTML = `
      <div class="small muted">Página ${page} de ${totalPages} · Registros: ${totalRows}</div>
      <div class="d-flex gap-2">
        <button class="btn toolbar-btn btn-sm" type="button" id="btnMhPrev" ${page <= 1 ? 'disabled' : ''}>Anterior</button>
        <button class="btn toolbar-btn btn-sm" type="button" id="btnMhNext" ${page >= totalPages ? 'disabled' : ''}>Siguiente</button>
      </div>
    `;
    if ($('btnMhPrev')) $('btnMhPrev').addEventListener('click', () => loadMovementHistoryPage(page - 1));
    if ($('btnMhNext')) $('btnMhNext').addEventListener('click', () => loadMovementHistoryPage(page + 1));
  }
}

function renderMoves(rows){
  const tb = $('movesBody');
  if (!tb) return;
  tb.innerHTML = '';

  if (!rows.length) {
    $('emptyHint').textContent = 'Sin movimientos registrados.';
    return;
  }
  $('emptyHint').textContent = '';

  for (const r of rows) {
    const typ = r.move_type === 'OUT' ? 'Salida' : (r.move_type === 'IN' ? 'Entrada' : 'Ajuste');
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td><div class="mono fw-semibold">${esc(r.work_date || '')}</div><div class="small muted mono">${esc(String(r.created_at || '').substring(11,19))}</div></td>
        <td>
          <div class="fw-semibold" >${esc(r.sku)}</div>
          <div class="small" >${esc(r.product_name || '')}</div>
        </td>
        <td>${esc(typ)}</td>
        <td>
          <div class="mono fw-semibold">${esc(r.warehouse_code || '-')}</div>
          <div class="small muted">${esc(r.warehouse_name || '')}</div>
        </td>
        <td>
          <div class="mono">${esc(r.ref_type || r.channel_name || '-')}</div>
          <div class="small muted">${r.ref_id ? ('#' + esc(r.ref_id)) : ''}</div>
        </td>
        <td>
          <div class="small fw-semibold">${esc(r.user_name || r.username || '-')}</div>
          <div class="small muted mono">${esc(r.username || '')}</div>
        </td>
        <td class="text-end mono fw-bold">${fmt(r.qty)}</td>
        <td class="small" >${esc(r.notes || '')}</td>
        <td class="text-end">
          <span class="small muted">Histórico</span>
        </td>
      </tr>
    `);
  }
}

function renderSalesOrders(){
  const tb = $('salesBody');
  tb.innerHTML = '';

  if (!salesRows.length) {
    $('salesEmptyHint').textContent = 'Sin pedidos autorizada de VentasHielo pendientes de surtir.';
    return;
  }

  $('salesEmptyHint').textContent = '';

  salesRows.forEach(r => {
    const folio = r.sales_order || r.ov_system || ('PW-' + r.id);
    const fecha = r.delivery_date || '';
    const hora = r.delivery_time || '';
    const cliente = r.cliente || '';
    const destino = r.address_label && r.address_label !== '—' ? r.address_label : (r.delivery_address || '');
    const statusTxt = String(r.status || 'autorizada').toUpperCase();

    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="mono fw-bold">${esc(folio)}</td>
        <td>
          <div>${esc(fecha)}</div>
          <div class="small">${esc(hora)}</div>
        </td>
        <td>
          <div class="fw-semibold" >${esc(cliente)}</div>
          <div class="small" >${esc(destino)}</div>
        </td>
        <td class="mono text-center">${esc(r.items_count || 0)}</td>
        <td class="text-center mono fw-bold">${esc(r.Qty_pza || '')}</td>
        <td><span class="status-chip status-autorizada">${esc(statusTxt)}</span></td>
        <td class="text-end">
          <button class="btn btn-gold-soft btn-sm" type="button" data-sales-detail="${r.id}" ${CAN_WRITE ? '' : 'disabled'}>
            <i class="bi bi-eye me-1"></i>Ver / Surtir
          </button>
        </td>
      </tr>
    `);
  });
}



function isRouteLogisticsOrder(row){
  const folio = String(row?.sales_order_ref || row?.reference || '').toUpperCase();
  const customer = String(row?.business_name || row?.contact_name || row?.customer_name || '').toUpperCase();
  const address = String(row?.delivery_address || '').toUpperCase();
  const comments = String(row?.comments || '').toUpperCase();
  return folio.startsWith('OSA-') || customer.startsWith('RUTA ') || address.includes('CARGA A RUTA') || comments.includes('ORDEN DE SALIDA CREADA DESDE ALMACÉN BOTELLA');
}
function routeNoFromAnyRow(row){
  const direct = parseInt(row?.route_no || 0, 10);
  if (direct > 0) return direct;

  const txt = [
    row?.reference,
    row?.sales_order_ref,
    row?.purchase_order,
    row?.delivery_address,
    row?.comments,
    row?.notes,
    row?.channel_name,
    row?.business_name,
    row?.contact_name
  ].filter(Boolean).join(' | ');

  const patterns = [
    /RUTA[-_\s#:]*([0-9]+)/i,
    /RUTA\s*DLX[:\s#-]*([0-9]+)/i,
    /RUTA\s+DESTINO[:\s#-]*([0-9]+)/i,
    /CARGA\s+A\s+RUTA[:\s#-]*([0-9]+)/i
  ];
  for (const re of patterns) {
    const m = txt.match(re);
    if (m && parseInt(m[1], 10) > 0) return parseInt(m[1], 10);
  }
  return 0;
}

function routeLoadUrlFromRow(row){
  const routeNo = routeNoFromAnyRow(row) || '';
  const date = row?.requested_delivery_date || row?.outbound_date || row?.work_date || $('salesRouteDate')?.value || operationalDateForForms();
  const params = new URLSearchParams();
  if (routeNo) params.set('route_no', routeNo);
  if (date) params.set('work_date', date);
  return 'carga_ruta_botella.php' + (params.toString() ? '?' + params.toString() : '');
}
function commercialTruckUrlFromRow(row){
  const orderId = parseInt(row?.id || 0, 10) || '';
  return 'carga_camion_botella.php' + (orderId ? '?order_id=' + encodeURIComponent(orderId) : '');
}

function routeLabel(routeNo){
  const n = parseInt(routeNo || 0, 10);
  return n > 0 ? `Ruta ${n}` : 'Sin ruta';
}
function routeStatusInfo(group){
  const pending = parseInt(group.pending_count || 0, 10);
  const dispatched = parseInt(group.dispatched_count || 0, 10);
  if (pending > 0 && dispatched > 0) return {cls:'mixed', txt:'Parcial', icon:'bi-exclamation-circle'};
  if (pending > 0) return {cls:'pending', txt:'Pendiente de salida', icon:'bi-x-circle'};
  return {cls:'done', txt:'Salida generada', icon:'bi-check2-circle'};
}
function renderSalesRouteCards(){
  const wrap = $('salesRouteCards');
  if (!wrap) return;
  wrap.innerHTML = '';
  if (!salesRouteGroups.length) {
    $('salesRouteEmptyHint').textContent = 'Sin pedidos de ruta para la fecha seleccionada.';
    wrap.innerHTML = `<div class="col-12"><div class="route-empty"><i class="bi bi-calendar-x fs-3 d-block mb-2"></i>No hay rutas activas para esta fecha.</div></div>`;
    return;
  }
  $('salesRouteEmptyHint').textContent = '';
  salesRouteGroups.forEach(g => {
    const st = routeStatusInfo(g);
    const routeNo = parseInt(g.route_no || 0, 10);
    wrap.insertAdjacentHTML('beforeend', `
      <div class="col-12 col-md-6 col-xl-4">
        <button class="route-card route-${st.cls}" type="button" data-route-card="${esc(routeNo)}">
          <div class="d-flex justify-content-between align-items-start gap-2 mb-3">
            <div><div class="route-meta">Ruta de entrega</div><div class="route-number">${esc(routeLabel(routeNo))}</div></div>
            <span class="route-badge ${st.cls}"><i class="bi ${st.icon}"></i>${esc(st.txt)}</span>
          </div>
          <div class="row g-2">
            <div class="col-6"><div class="route-meta">Pedidos</div><div class="fw-bold mono">${esc(g.orders_count || 0)}</div></div>
            <div class="col-6"><div class="route-meta">Artículos</div><div class="fw-bold mono">${esc(g.items_count || 0)}</div></div>
            <div class="col-6"><div class="route-meta">Pendientes</div><div class="fw-bold mono text-danger">${esc(g.pending_count || 0)}</div></div>
            <div class="col-6"><div class="route-meta">Surtidos</div><div class="fw-bold mono text-success">${esc(g.dispatched_count || 0)}</div></div>
          </div>
          <div class="route-meta mt-3">Cantidad total: <b class="mono">${fmt(g.total_qty || 0)}</b></div>
        </button>
      </div>
    `);
  });
}
function openRouteOrders(routeNo){
  const rn = parseInt(routeNo || 0, 10);
  selectedRouteGroup = salesRouteGroups.find(g => parseInt(g.route_no || 0, 10) === rn) || null;
  if (!selectedRouteGroup) return;
  const st = routeStatusInfo(selectedRouteGroup);
  $('routeOrdersTitle').textContent = `${routeLabel(rn)} · ${st.txt}`;
  $('routeOrdersSub').textContent = `Fecha ${$('salesRouteDate').value || ''} · ${selectedRouteGroup.orders_count || 0} pedido(s)`;
  const list = $('routeOrdersList');
  list.innerHTML = '';
  (selectedRouteGroup.orders || []).forEach(o => {
    const folio = o.sales_order_ref || ('OSV-' + String(o.id).padStart(6, '0'));
    const done = !!o.outbound_id || String(o.status || '').toUpperCase() !== 'AUTORIZADA';
    const cls = done ? 'done' : 'pending';
    const statusTxt = done ? 'Salida generada' : 'Pendiente de salida';
    const pdfUrl = `./orden_salida_botella_pdf.php?formato=carta&order_id=${encodeURIComponent(o.id)}&outbound_id=${encodeURIComponent(o.outbound_id || '')}&folio=${encodeURIComponent(folio)}`;
    const ticketUrl = `./orden_salida_botella_pdf.php?formato=ticket&order_id=${encodeURIComponent(o.id)}&outbound_id=${encodeURIComponent(o.outbound_id || '')}&folio=${encodeURIComponent(folio)}`;
    list.insertAdjacentHTML('beforeend', `
      <div class="col-12 col-lg-6">
        <div class="route-order-card ${cls}">
          <div class="d-flex justify-content-between align-items-start gap-2 mb-2">
            <div><div class="fw-bold mono fs-5">${esc(folio)}</div><div class="small muted">${esc(o.requested_delivery_date || '')} ${esc(o.requested_delivery_time || '')}</div></div>
            <span class="route-badge ${done ? 'done' : 'pending'}"><i class="bi ${done ? 'bi-check2-circle' : 'bi-x-circle'}"></i>${statusTxt}</span>
          </div>
          <div class="fw-semibold">${esc(o.business_name || '')}</div>
          <div class="small muted mb-2">${esc(o.delivery_address || '')}</div>
          <div class="row g-2 mb-3">
            <div class="col-4"><div class="route-meta">Items</div><div class="fw-bold mono">${esc(o.items_count || 0)}</div></div>
            <div class="col-4"><div class="route-meta">Cantidad</div><div class="fw-bold mono">${fmt(o.total_qty || 0)}</div></div>
            <div class="col-4"><div class="route-meta">Salida</div><div class="fw-bold mono">${esc(o.outbound_id || '-')}</div></div>
          </div>
          <div class="d-flex gap-2 flex-wrap justify-content-end">
            ${done ? `<a class="btn btn-green-soft btn-sm" href="${pdfUrl}" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf me-1"></i>PDF carta</a>` : ''}
            ${done ? `<a class="btn btn-blue-soft btn-sm" href="${ticketUrl}" target="_blank" rel="noopener"><i class="bi bi-receipt me-1"></i>Ticket</a>` : ''}
            ${!done ? `<a class="btn btn-primary-soft btn-sm" href="${routeLoadUrlFromRow(o)}" target="_blank" rel="noopener"><i class="bi bi-truck-front me-1"></i>Carga ruta</a>` : ''}
            <button class="btn ${done ? 'btn-blue-soft' : 'btn-gold-soft'} btn-sm" type="button" data-breport-sales-detail="${esc(o.id)}" ${CAN_WRITE || done ? '' : 'disabled'}>
              <i class="bi bi-eye me-1"></i>${done ? 'Ver detalle' : 'Ver / Generar salida'}
            </button>
          </div>
        </div>
      </div>
    `);
  });
  routeOrdersModal.show();
}

function dispatchStatusLabel(folder, row){
  if (folder === 'warehouse') return {text:'GENERADA', cls:'manual'};
  if (String(row.status || '').toUpperCase() === 'ENTREGADA') return {text:'ENTREGADA', cls:'done'};
  if (row.outbound_id) return {text:'SALIDA GENERADA', cls:'done'};
  const st = String(row.status || '').toUpperCase();
  const fl = String(row.forklift_status || '').toUpperCase();
  if (fl === 'COMPLETA') return {text:'LISTA PARA SALIR CON EL CLIENTE', cls:'pending'};
  if (st === 'SURTIENDO' || fl === 'PENDIENTE' || fl === 'EN_PROCESO') return {text:'CARGANDO UNIDAD', cls:'manual'};
  if (st === 'AUTORIZADA') return {text:'POR SURTIR', cls:'pending'};
  return {text: st || 'PENDIENTE', cls:'done'};
}

function dispatchRowDate(folder, row){
  if (folder === 'vh') return row.delivery_date || '';
  if (folder === 'sales') return row.requested_delivery_date || '';
  return row.outbound_date || '';
}

function dispatchRowFolio(folder, row){
  if (folder === 'vh') return row.sales_order || row.ov_system || ('PW-' + row.id);
  if (folder === 'sales') return row.sales_order_ref || ('OSV-' + row.id);
  return row.reference || ('SAL-' + row.id);
}

function dispatchRowCustomer(folder, row){
  if (folder === 'vh') return row.cliente || 'Cliente VentasHielo';
  if (folder === 'sales') return row.business_name || row.contact_name || 'Cliente';
  return row.channel_name || 'Salida manual de almacén';
}

function dispatchSalesOC(row){
  const oc = String(row.purchase_order || row.customer_po || '').trim();
  return oc !== '' ? oc : '—';
}

function cleanSalesUserNotes(raw){
  let txt = String(raw || '').trim();
  if (!txt) return '';
  txt = txt.replace(/\r/g, '\n');
  txt = txt.split('\n').map(x => x.trim()).filter(Boolean).join(' ');
  txt = txt.replace(/^Orden\s+de\s+salida\s+Botella\s+creada\s+desde\s+Ventas\.?\s*/i, '');
  txt = txt.replace(/^Ruta:\s*RUTA-VENTAS\s*[·\-]\s*RUTA\s+DE\s+CARGA\s+DE\s+CLIENTES\s*/i, '');
  txt = txt.replace(/^Ruta:\s*RUTA-VENTAS\s*[·\-]\s*/i, '');
  txt = txt.replace(/^Ruta:\s*/i, '');
  txt = txt.trim();
  return txt;
}

function itemPackagingLabel(it){
  const pkgName = String(it.packaging_name || it.packaging_type || '').trim();
  const ppp = Number(it.packages_per_pallet || 0);
  const palletType = String(it.pallet_type || '').trim();
  const palletSpec = String(it.pallet_specification || '').trim();
  const parts = [];
  if (pkgName) parts.push(pkgName);
  else if (ppp > 0) parts.push(`Tarima ${ppp} paquetes`);
  if (palletType || palletSpec) parts.push(`${palletType} ${palletSpec}`.trim());
  return parts.join(' · ') || '—';
}

function renderDispatchMailboxList(){
  const box = $('dispatchMailboxItems');
  if (!box) return;
  const folder = dispatchMailboxFolder;
  const rows = dispatchMailboxRows[folder] || [];
  const titles = {
    vh: ['VentasHielo · por surtir', 'Pedidos autorizados pendientes de generar salida'],
    sales: ['Salidas de Ventas', 'Órdenes de venta de la planta seleccionada'],
    warehouse: ['Salidas de Almacén', 'Movimientos capturados directamente por almacén']
  };
  $('dispatchFolderTitle').textContent = titles[folder][0];
  const sub = $('dispatchFolderSub');
  const allRows = dispatchMailboxRowsAll[folder] || rows;
  if (folder === 'sales') {
    const processedCount = (allRows || []).filter(dispatchIsProcessedSalesOrder).length;
    const pendingCount = Math.max(0, (allRows || []).length - processedCount);
    sub.innerHTML = `
      <span>${rows.length} registro(s) visibles · ${pendingCount} pendiente(s) · ${processedCount} procesada(s) · ${dispatchMailboxAllDates ? 'todos los días' : ($('dispatchMailboxDate')?.value || 'día seleccionado')}</span>
      ${processedCount > 0 ? `<button class="btn btn-sm ${dispatchShowProcessedSales ? 'btn-blue-soft' : 'toolbar-btn'} ms-2 py-1" type="button" id="btnToggleProcessedSalesOrders">
        <i class="bi ${dispatchShowProcessedSales ? 'bi-eye-slash' : 'bi-eye'} me-1"></i>${dispatchShowProcessedSales ? 'Ocultar procesadas' : 'Ver procesadas'}
      </button>` : ''}
    `;
    const toggle = $('btnToggleProcessedSalesOrders');
    if (toggle) {
      toggle.addEventListener('click', () => {
        dispatchShowProcessedSales = !dispatchShowProcessedSales;
        applyDispatchSalesProcessedFilter();
        renderDispatchMailboxList();
      });
    }
  } else {
    sub.textContent = `${rows.length} registro(s) · ${dispatchMailboxAllDates ? 'todos los días' : ($('dispatchMailboxDate')?.value || 'día seleccionado')}`;
  }

  if (!rows.length) {
    const allSales = dispatchMailboxRowsAll.sales || [];
    const hiddenProcessed = folder === 'sales' && !dispatchShowProcessedSales && allSales.length > 0 && allSales.every(dispatchIsProcessedSalesOrder);
    box.innerHTML = hiddenProcessed
      ? `<div class="dispatch-empty"><i class="bi bi-check2-circle fs-2 mb-2"></i><div class="fw-bold">Todas las órdenes están procesadas.</div><div class="small muted mt-1">Usa el botón <b>Ver procesadas</b> para consultarlas.</div></div>`
      : `<div class="dispatch-empty"><i class="bi bi-inbox fs-2 mb-2"></i><div>No hay salidas en esta carpeta.</div></div>`;
    $('dispatchMailboxDetail').innerHTML = `<div class="dispatch-empty"><i class="bi bi-envelope-open fs-1 mb-2"></i><div class="fw-bold">Sin registros para mostrar</div></div>`;
    return;
  }

  box.innerHTML = rows.map((r, idx) => {
    const status = dispatchStatusLabel(folder, r);
    const folio = dispatchRowFolio(folder, r);
    const customer = dispatchRowCustomer(folder, r);
    const ocText = folder === 'sales' ? dispatchSalesOC(r) : '';
    const date = dispatchRowDate(folder, r);
    const qty = fmt(r.total_qty ?? r.Qty_pza ?? 0);
    const stockStatus = folder === 'sales' ? String(r.stock_status || '').toLowerCase() : '';
    const stockLabel = folder === 'sales' ? String(r.stock_label || '') : '';
    const stockClass = stockStatus ? ` stock-${stockStatus}` : '';
    const stockHtml = folder === 'sales' && stockLabel
      ? `<span class="stock-pill ${esc(stockStatus)}">${esc(stockLabel)}</span>`
      : `<span class="dispatch-status ${status.cls}">${esc(status.text)}</span>`;
    return `
      <button type="button" class="dispatch-item ${stockClass} ${idx === 0 ? 'active' : ''}" data-dispatch-index="${idx}">
        <div class="d-flex justify-content-between align-items-start gap-2">
          <div class="dispatch-item-title mono">${esc(folio)}</div>
          ${stockHtml}
        </div>
        <div class="dispatch-item-sub">${folder === 'sales' ? `OC ${esc(ocText)} · ${esc(customer)}` : esc(customer)}</div>
        <div class="dispatch-item-bottom">
          <span class="mono">${esc(date || 'Sin fecha')}</span>
          <span class="mono fw-bold">${esc(qty)} pz</span>
        </div>
        ${folder === 'sales' && r.stock_message ? `<div class="stock-line ${esc(stockStatus)}">${esc(r.stock_message)}</div>` : ''}
      </button>
    `;
  }).join('');

  selectDispatchMailboxRow(0);
}

async function selectDispatchMailboxRow(index){
  const folder = dispatchMailboxFolder;
  const rows = dispatchMailboxRows[folder] || [];
  const row = rows[index];
  if (!row) return;

  document.querySelectorAll('#dispatchMailboxItems .dispatch-item').forEach((el, i) => {
    el.classList.toggle('active', i === index);
  });

  dispatchMailboxSelected = {folder, row};
  const status = dispatchStatusLabel(folder, row);
  const folio = dispatchRowFolio(folder, row);
  const customer = dispatchRowCustomer(folder, row);
  const qty = fmt(row.total_qty ?? row.Qty_pza ?? 0);
  const salesOC = dispatchSalesOC(row);
  let salesNotes = cleanSalesUserNotes(row.comments || '');
  let itemsHtml = '';
  let actionHtml = '';
  let tableHeadHtml = '<tr><th>Artículos / detalle</th><th class="text-end">Cantidad</th><th>UOM</th></tr>';

  if (folder === 'warehouse') {
    const detail = await apiGet('dispatch_mailbox_outbound_detail', {outbound_id: row.id});
    if (!detail.ok) {
      $('dispatchMailboxDetail').innerHTML = `<div class="dispatch-empty">No fue posible consultar el detalle de la salida.</div>`;
      return;
    }
    itemsHtml = (detail.items || []).map(it => `
      <tr>
        <td><div class="mono fw-bold">${esc(it.sku || '')}</div><div class="small muted">${esc(it.product_name || '')}</div></td>
        <td class="text-end mono fw-bold">${fmt(it.qty)}</td>
        <td>${esc(it.uom || '')}</td>
      </tr>
    `).join('');
    const routeUrl = routeLoadUrlFromRow(row);
    const routeNo = routeNoFromAnyRow(row);
    actionHtml = `
      <button class="btn toolbar-btn" type="button" id="btnDispatchRegisterNew"><i class="bi bi-plus-lg me-1"></i>Nueva salida manual</button>
      <a class="btn btn-primary-soft" href="${routeUrl}" target="_blank" rel="noopener">
        <i class="bi bi-truck-front me-1"></i>${routeNo > 0 ? 'Abrir carga ruta' : 'Crear carga ruta'}
      </a>`;
  } else if (folder === 'vh') {
    itemsHtml = `<tr><td colspan="3" class="small muted">Usa “Ver / surtir” para consultar artículos e inventario disponible antes de registrar la salida.</td></tr>`;
    actionHtml = `<button class="btn btn-green-soft" type="button" data-dispatch-open-vh="${esc(row.id)}"><i class="bi bi-box-arrow-right me-1"></i>Ver / surtir</button>`;
  } else {
    const routeOrder = isRouteLogisticsOrder(row);
    const detail = await apiGet('sales_order_detail', {order_id: row.id});
    let detailOrder = Object.assign({}, row);
    let detailForkliftStatus = String(row.forklift_status || '').toUpperCase();
    if (detail.ok) {
      const order = detail.order || {};
      detailOrder = Object.assign({}, row, order);
      detailForkliftStatus = String((detail.forklift_order && detail.forklift_order.status) || order.forklift_status || row.forklift_status || '').toUpperCase();
      const orderOC = String(order.purchase_order || salesOC || '').trim() || '—';
      salesNotes = cleanSalesUserNotes(order.comments || row.comments || '');
      tableHeadHtml = '<tr><th>OC</th><th>Artículo</th><th>Embalaje</th><th class="text-end">Paquetes</th><th class="text-end">Tarimas</th><th class="text-end">Botellas</th></tr>';
      const stockByProduct = {};
      (row.stock_items || []).forEach(si => { stockByProduct[String(si.product_id)] = si; });
      itemsHtml = (detail.items || []).map(it => {
        const itemOC = String(it.purchase_order || orderOC || '').trim() || '—';
        const packagesQty = Number(it.packages_qty || 0);
        const palletsQty = Number(it.pallets_qty || 0);
        const bottlesQty = Number(it.qty || 0);
        const stockInfo = stockByProduct[String(it.product_id || '')] || null;
        const stockLine = stockInfo
          ? `<div class="stock-line ${esc(String(stockInfo.stock_status || '').toLowerCase())}">
              ALM01 disp. antes: ${fmt(stockInfo.available_before || 0)} · solicitado acumulado: ${fmt(stockInfo.requested_qty || 0)} · después: ${fmt(stockInfo.available_after || 0)}
            </div>`
          : '';
        return `
          <tr>
            <td class="mono fw-bold">${esc(itemOC)}</td>
            <td><div class="mono fw-bold">${esc(it.sku || '')}</div><div class="small muted">${esc(it.product_name || '')}</div>${stockLine}</td>
            <td><div>${esc(itemPackagingLabel(it))}</div></td>
            <td class="text-end mono fw-bold">${fmt(packagesQty)}</td>
            <td class="text-end mono fw-bold">${fmt(palletsQty)}</td>
            <td class="text-end mono fw-bold">${fmt(bottlesQty)}</td>
          </tr>
        `;
      }).join('') || `<tr><td colspan="6" class="small muted">La orden no tiene artículos registrados.</td></tr>`;
    } else {
      tableHeadHtml = '<tr><th>Artículos / detalle</th><th class="text-end">Cantidad</th><th>UOM</th></tr>';
      itemsHtml = `<tr><td colspan="3" class="small muted">No fue posible consultar los artículos de la orden.</td></tr>`;
    }

    const detailStatus = String(detailOrder.status || row.status || '').toUpperCase();
    const detailOutboundId = detailOrder.outbound_id || row.outbound_id || '';
    const hasGeneratedOutbound = !!detailOutboundId || detailStatus === 'ENTREGADA' || detailStatus === 'SURTIENDO';
    const forkliftReadyForClient = detailForkliftStatus === 'COMPLETA' || status.text === 'LISTA PARA SALIR CON EL CLIENTE';
    const canShowTruckLoad = !hasGeneratedOutbound && !forkliftReadyForClient && detailStatus === 'AUTORIZADA';
    const pdfFolio = detailOrder.sales_order_ref || folio;
    const pdfUrl = `./orden_salida_botella_pdf.php?formato=carta&order_id=${encodeURIComponent(row.id)}&outbound_id=${encodeURIComponent(detailOutboundId || '')}&folio=${encodeURIComponent(pdfFolio)}`;
    const ticketUrl = `./orden_salida_botella_pdf.php?formato=ticket&order_id=${encodeURIComponent(row.id)}&outbound_id=${encodeURIComponent(detailOutboundId || '')}&folio=${encodeURIComponent(pdfFolio)}`;
    const pdfActions = hasGeneratedOutbound
      ? `<a class="btn btn-green-soft" href="${pdfUrl}" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf me-1"></i>PDF carta</a>
         <a class="btn btn-blue-soft" href="${ticketUrl}" target="_blank" rel="noopener"><i class="bi bi-receipt me-1"></i>Ticket</a>`
      : '';
    const truckAction = canShowTruckLoad
      ? (routeOrder
        ? `<a class="btn btn-primary-soft" href="${routeLoadUrlFromRow(row)}" target="_blank" rel="noopener"><i class="bi bi-truck-front me-1"></i>Carga ruta</a>`
        : `<button class="btn btn-primary-soft" type="button" data-botella-truck-plan="${esc(row.id)}"><i class="bi bi-truck-front me-1"></i>Carga camión</button>`)
      : '';

    actionHtml = `<button class="btn ${status.cls === 'pending' ? 'btn-green-soft' : 'btn-blue-soft'}" type="button" data-dispatch-open-sales="${esc(row.id)}"><i class="bi bi-eye me-1"></i>${status.cls === 'pending' ? 'Ver / surtir' : 'Ver detalle'}</button>
      ${truckAction}
      ${pdfActions}`;
  }

  const detailStockStatus = folder === 'sales' ? String(row.stock_status || '').toLowerCase() : '';
  const detailStockAdvice = folder === 'sales' && row.stock_message
    ? `<div class="stock-advice ${esc(detailStockStatus)}"><i class="bi bi-lightbulb me-1"></i>${esc(row.stock_message)}</div>`
    : '';

  $('dispatchMailboxDetail').innerHTML = `
    ${detailStockAdvice}
    <div class="d-flex justify-content-between gap-2 align-items-start flex-wrap mb-3">
      <div>
        <div class="small muted">Folio / referencia</div>
        <div class="fw-bold fs-5 mono">${esc(folio)}</div>
        <div class="muted small">${esc(customer)}</div>
      </div>
      <span class="dispatch-status ${status.cls}">${esc(status.text)}</span>
    </div>
    <div class="row g-2 mb-3">
      ${folder === 'sales' ? `
      <div class="col-12 col-md-4">
        <div class="dispatch-detail-card h-100">
          <div class="dispatch-detail-label">OC</div>
          <div class="dispatch-detail-value mono">${esc(salesOC)}</div>
        </div>
      </div>` : ''}
      <div class="${folder === 'sales' ? 'col-6 col-md-4' : 'col-6'}">
        <div class="dispatch-detail-card h-100">
          <div class="dispatch-detail-label">Fecha</div>
          <div class="dispatch-detail-value mono">${esc(dispatchRowDate(folder, row) || '—')}</div>
        </div>
      </div>
      <div class="${folder === 'sales' ? 'col-6 col-md-4' : 'col-6'}">
        <div class="dispatch-detail-card h-100">
          <div class="dispatch-detail-label">Cantidad</div>
          <div class="dispatch-detail-value mono">${esc(qty)} pz</div>
        </div>
      </div>
      <div class="col-12">
        <div class="dispatch-detail-card">
          <div class="dispatch-detail-label">${folder === 'warehouse' ? 'Canal / usuario' : 'Destino / información'}</div>
          <div class="dispatch-detail-value">${esc(folder === 'warehouse' ? ((row.channel_name || 'Salida manual') + ' · ' + (row.user_name || '')) : (row.delivery_address || row.address_label || customer))}</div>
        </div>
      </div>
      ${folder === 'sales' ? `
      <div class="col-12">
        <div class="dispatch-detail-card">
          <div class="dispatch-detail-label">Notas registradas en Ventas</div>
          <div class="dispatch-detail-value">${esc(salesNotes || 'Sin notas registradas')}</div>
        </div>
      </div>` : ''}
    </div>
    <div class="table-wrap mb-3">
      <table class="table table-sm align-middle mb-0">
        <thead>${tableHeadHtml}</thead>
        <tbody>${itemsHtml}</tbody>
      </table>
    </div>
    <div class="d-flex gap-2 flex-wrap">${actionHtml}</div>
  `;

  if ($('btnDispatchRegisterNew')) {
    if ($('btnDispatchRegisterNew')) $('btnDispatchRegisterNew').addEventListener('click', () => { if (CAN_WRITE) openModal(); });
  }
}

async function loadDispatchMailbox(){
  const date = dispatchMailboxAllDates ? '' : ($('dispatchMailboxDate')?.value || operationalDateForForms());

  let vhRows = [];
  if (IS_HIELO_BARRA_SCOPE) {
    const vh = await apiGet('vh_authorized', {page:1, pageSize:300});
    if (vh.ok) {
      vhRows = (vh.rows || []).filter(r => date === '' || String(r.delivery_date || '') === date);
    }
  }

  const [sales, warehouse] = await Promise.all([
    apiGet('dispatch_mailbox_sales', {date}),
    apiGet('dispatch_mailbox_outbounds', {date})
  ]);

  dispatchMailboxRowsAll.vh = vhRows;
  dispatchMailboxRowsAll.sales = sales.ok ? (sales.rows || []) : [];
  dispatchMailboxRowsAll.warehouse = warehouse.ok ? (warehouse.rows || []) : [];
  dispatchMailboxRows.vh = dispatchMailboxRowsAll.vh;
  applyDispatchSalesProcessedFilter();
  dispatchMailboxRows.warehouse = dispatchMailboxRowsAll.warehouse;

  const visibleSales = dispatchMailboxRows.sales || [];
  const allSales = dispatchMailboxRowsAll.sales || [];
  const pendingSales = allSales.filter(r => !dispatchIsProcessedSalesOrder(r)).length;
  const processedSales = allSales.length - pendingSales;

  if ($('dispatchCountVH')) $('dispatchCountVH').textContent = String(dispatchMailboxRows.vh.length);
  if ($('dispatchCountSales')) $('dispatchCountSales').textContent = String(pendingSales);
  if ($('dispatchCountWarehouse')) $('dispatchCountWarehouse').textContent = String(dispatchMailboxRows.warehouse.length);

  const totalPending = dispatchMailboxRows.vh.length + pendingSales;
  if ($('countDispatchMailbox')) $('countDispatchMailbox').textContent = String(totalPending);

  const suggestion = $('dispatchSuggestion');
  if (suggestion) {
    const salesRows = (dispatchMailboxRowsAll.sales || []).filter(r => !dispatchIsProcessedSalesOrder(r));
    const stockDanger = salesRows.filter(r => String(r.stock_status || '').toLowerCase() === 'danger').length;
    const stockWarning = salesRows.filter(r => String(r.stock_status || '').toLowerCase() === 'warning').length;
    const stockStable = salesRows.filter(r => String(r.stock_status || '').toLowerCase() === 'stable').length;
    if (stockDanger > 0) {
      suggestion.innerHTML = `<b>Sugerencia inteligente:</b> ${stockDanger} orden(es) ya no se alcanzarían a surtir con ALM01 considerando las órdenes pendientes anteriores. Revisa existencias o recarga antes de continuar.`;
    } else if (stockWarning > 0) {
      suggestion.innerHTML = `<b>Sugerencia inteligente:</b> ${stockWarning} orden(es) se pueden surtir, pero dejarían inventario bajo en ALM01. Prioriza por fecha/folio y valida existencias.`;
    } else if (totalPending > 0) {
      suggestion.innerHTML = `<b>Sugerencia inteligente:</b> hay ${totalPending} salida(s) pendientes y ${stockStable} con inventario estable en ALM01. Puedes surtir en el orden mostrado.`;
    } else {
      suggestion.innerHTML = `<b>Bandeja al día:</b> no hay pedidos autorizados pendientes en el filtro actual.`;
    }
  }

  renderDispatchMailboxList();
}

async function loadSalesCreatedOrders(){
  const date = $('salesRouteDate')?.value || operationalDateForForms();
  const j = await apiGet('sales_route_orders', {date});
  if (!j.ok) {
    alert(j.error || 'Error al cargar rutas creadas desde ventas');
    return;
  }
  salesRouteGroups = j.routes || [];
  salesCreatedRows = j.rows || [];
  renderSalesRouteCards();
}

function updateModuleCounts(moveRows = [], mermaRowsLocal = []){
  const available = invRows.filter(x => num(x.on_hand) > 0).length;
  if ($('countInventory')) $('countInventory').textContent = String(available);
  if ($('countVH')) $('countVH').textContent = String(salesTotal || salesRows.length || 0);
  if ($('countSalesCreated')) $('countSalesCreated').textContent = String(salesCreatedRows.length || 0);
  if ($('countReturns')) $('countReturns').textContent = String((moveRows || []).filter(x => String(x.ref_type || '').toUpperCase() === 'WAREHOUSE_RETURN').length);
  if ($('countLocReq')) $('countLocReq').textContent = String((locReqRows || []).length);
}


function locReqStatusBadge(status){
  const s = String(status || '').toUpperCase();
  const cls = s === 'PENDIENTE' ? 'text-bg-warning' : s === 'ENVIADA' ? 'text-bg-info' : s === 'RECIBIDA' ? 'text-bg-success' : s === 'RECHAZADA' ? 'text-bg-danger' : 'text-bg-secondary';
  return `<span class="badge ${cls}">${esc(s)}</span>`;
}

function createLocReqItemRow(){
  const div = document.createElement('div');
  div.className = 'item-row';
  div.innerHTML = `
    <div class="row g-3 align-items-end">
      <div class="col-12 col-md-8">
        <label class="form-label">Producto</label>
        <select class="form-select lr-product" required>
          ${products.map(p => `<option value="${p.id}">${esc(p.sku)} · ${esc(p.name || '')}</option>`).join('')}
        </select>
      </div>
      <div class="col-8 col-md-3">
        <label class="form-label">Cantidad</label>
        <input class="form-control mono lr-qty" inputmode="decimal" value="0" required>
      </div>
      <div class="col-4 col-md-1 text-end">
        <button class="btn btn-outline-danger" type="button" title="Quitar" onclick="this.closest('.item-row').remove()"><i class="bi bi-x-lg"></i></button>
      </div>
    </div>`;
  return div;
}


function locReqActorText(r){
  const status = String(r?.status || '').toUpperCase();
  if (status === 'RECHAZADA') {
    return r.rejected_by_name ? `Rechazó: ${esc(r.rejected_by_name)}` : 'Rechazó: —';
  }
  if (r.approved_by_name) return `Autorizó/envió: ${esc(r.approved_by_name)}`;
  if (r.shipped_by_name) return `Envió: ${esc(r.shipped_by_name)}`;
  return 'Pendiente de autorización/envío';
}

function openLocReqConfirmModal({title='Confirmar', sub='Valida la información antes de continuar.', body='', okText='Confirmar', okClass='btn btn-blue-soft', onConfirm=null}){
  $('locReqConfirmTitle').textContent = title;
  $('locReqConfirmSub').textContent = sub;
  $('locReqConfirmBody').innerHTML = body;
  const ok = $('locReqConfirmOk');
  ok.className = okClass;
  ok.innerHTML = okText;
  ok.onclick = async () => {
    if (typeof onConfirm === 'function') await onConfirm();
  };
  locReqConfirmModal.show();
}

async function loadLocReqLocations(){
  const j = await apiGet('locreq_locations');
  locReqLocations = j.ok ? (j.rows || []) : [];
}

async function loadLocRequests(mode = locReqMode){
  locReqMode = mode || 'received';
  const j = await apiGet('locreq_list', {mode: locReqMode});
  if (!j.ok) {
    alert(j.error || 'Error al cargar solicitudes entre localidades');
    return;
  }
  locReqRows = j.rows || [];
  renderLocRequests();
  if ($('countLocReq')) $('countLocReq').textContent = String(locReqRows.length);
}

function renderLocRequests(){
  const tb = $('locReqBody');
  if (!tb) return;
  tb.innerHTML = '';
  if (!locReqRows.length) {
    $('locReqEmptyHint').textContent = 'Sin solicitudes para mostrar.';
    return;
  }
  $('locReqEmptyHint').textContent = '';
  for (const r of locReqRows) {
    const tr = document.createElement('tr');
    tr.innerHTML = `
      <td class="mono">SL-${String(r.id).padStart(6,'0')}</td>
      <td><div>${esc(r.request_location_name || '')}</div><div class="small muted">${esc(r.requested_by_name || '')}</div></td>
      <td>${esc(r.source_location_name || '')}</td>
      <td><div class="small">${locReqActorText(r)}</div>${String(r.status || '').toUpperCase()==='RECHAZADA' && r.reject_reason ? '<div class="small text-danger">Motivo: '+esc(r.reject_reason)+'</div>' : ''}</td>
      <td>${locReqStatusBadge(r.status)}</td>
      <td><div class="small">${esc(r.item_summary || '')}</div></td>
      <td><div class="small">${esc(r.driver_name || '-')} / ${esc(r.vehicle_unit || '-')}</div></td>
      <td><div class="small">${esc(r.request_date || '')}${r.required_date ? '<br><span class="muted">Req: '+esc(r.required_date)+'</span>' : ''}</div></td>
      <td class="text-end"><button class="btn toolbar-btn btn-sm" type="button" data-locreq-detail="${r.id}"><i class="bi bi-eye me-1"></i>Ver</button></td>
    `;
    tb.appendChild(tr);
  }
}

async function openLocReqModal(){
  await loadLocReqLocations();
  const sel = $('lrSourceLocation');
  sel.innerHTML = locReqLocations.map(l => `<option value="${l.id}">${esc(l.name)} ${l.code ? '('+esc(l.code)+')' : ''}</option>`).join('');
  const wrap = $('locReqItemsWrap');
  wrap.innerHTML = '';
  wrap.appendChild(createLocReqItemRow());
  $('locReqForm').reset();
  if (!locReqLocations.length) {
    alert('No hay otra localidad activa en esta empresa para solicitar producto.');
    return;
  }
  locReqModal.show();
}

async function saveLocReq(){
  const f = $('locReqForm');
  const items = [...document.querySelectorAll('#locReqItemsWrap .item-row')].map(row => ({
    product_id: parseInt(row.querySelector('.lr-product')?.value || '0', 10),
    qty: num(row.querySelector('.lr-qty')?.value || 0)
  })).filter(x => x.product_id > 0 && x.qty > 0);
  if (!items.length) { alert('Agrega al menos un artículo con cantidad mayor a cero.'); return; }
  const payload = {
    source_location_id: parseInt(f.source_location_id.value || '0', 10),
    required_date: f.required_date.value || '',
    notes: f.notes.value || '',
    items
  };
  const j = await apiPost('locreq_create', payload);
  if (!j.ok) { alert(j.error || 'No se pudo crear la solicitud'); return; }
  locReqModal.hide();
  await loadLocRequests('created');
  showModuleSection('locReqSection');
}

function whOptionsFromStock(rows, fieldName, selectedId = 0){
  const list = rows || [];
  if (!list.length) return '<option value="0">Sin almacén configurado para este producto/localidad</option>';
  return list.map(w => `<option value="${w.warehouse_id}" ${Number(w.warehouse_id)===Number(selectedId)?'selected':''}>${esc(w.code)} · disponible ${fmt(w.qty || 0)}</option>`).join('');
}

async function openLocReqDetail(id){
  const j = await apiGet('locreq_detail', {id});
  if (!j.ok) { alert(j.error || 'Error al cargar solicitud'); return; }
  const r = j.request;
  const items = j.items || [];
  $('locReqDetailTitle').textContent = `Solicitud SL-${String(r.id).padStart(6,'0')} · ${String(r.status).toUpperCase()}`;
  $('locReqDetailSub').textContent = `${r.request_location_name} solicita a ${r.source_location_name}`;

  let html = `
    <div class="row g-3 mb-3">
      <div class="col-12 col-md-4"><div class="item-row"><div class="muted small">Solicita</div><div class="fw-bold">${esc(r.request_location_name)}</div><div class="small muted">Usuario: ${esc(r.requested_by_name || '')}</div></div></div>
      <div class="col-12 col-md-4"><div class="item-row"><div class="muted small">Proveedor</div><div class="fw-bold">${esc(r.source_location_name)}</div></div></div>
      <div class="col-12 col-md-4"><div class="item-row"><div class="muted small">Autorización / rechazo</div><div class="fw-bold">${String(r.status || '').toUpperCase()==='RECHAZADA' ? esc(r.rejected_by_name || 'Rechazo registrado') : esc(r.approved_by_name || r.shipped_by_name || 'Pendiente')}</div><div class="small muted">${r.approved_at ? 'Autorizado: '+esc(r.approved_at) : (r.rejected_at ? 'Rechazado: '+esc(r.rejected_at) : '')}</div></div></div>
    </div>
    ${String(r.status || '').toUpperCase()==='RECHAZADA' ? `<div class="alert alert-danger py-2"><strong>Motivo del rechazo:</strong> ${esc(r.reject_reason || 'Sin motivo registrado')}</div>` : ''}
    <div class="table-responsive"><table class="table table-sm align-middle">
      <thead><tr><th>Producto</th><th class="text-end">Solicitado</th><th class="text-end">Enviado</th><th class="text-end">Recibido</th><th>Almacén origen</th><th>Almacén destino</th></tr></thead><tbody>`;
  for (const it of items) {
    html += `<tr>
      <td><div class="fw-semibold">${esc(it.sku)}</div><div class="small muted">${esc(it.product_name || '')}</div></td>
      <td class="text-end mono">${fmt(it.requested_qty)}</td>
      <td class="text-end mono">${it.shipped_qty == null ? '-' : fmt(it.shipped_qty)}</td>
      <td class="text-end mono">${it.received_qty == null ? '-' : fmt(it.received_qty)}</td>
      <td>${esc(it.source_warehouse_code || '-')}</td>
      <td>${esc(it.destination_warehouse_code || '-')}</td>
    </tr>`;
  }
  html += `</tbody></table></div>`;

  const status = String(r.status || '').toUpperCase();
  if (status === 'PENDIENTE' || status === 'APROBADA') {
    html += `<hr><form id="locReqShipForm"><div class="section-title mb-2"><i class="bi bi-truck text-warning"></i> Confirmar envío</div>
      <div class="row g-3 mb-3">
        <div class="col-12 col-md-4"><label class="form-label">Fecha envío</label><input class="form-control" type="date" name="work_date" value="${operationalDateForForms()}"></div>
        <div class="col-12 col-md-4"><label class="form-label">Chofer</label><input class="form-control" name="driver_name" placeholder="Nombre"></div>
        <div class="col-12 col-md-4"><label class="form-label">Unidad</label><input class="form-control" name="vehicle_unit" placeholder="Unidad / placas"></div>
      </div>`;
    for (const it of items) {
      html += `<div class="item-row mb-2" data-ship-item="${it.id}">
        <div class="row g-3 align-items-end">
          <div class="col-12 col-md-5"><div class="fw-semibold">${esc(it.sku)}</div><div class="small muted">${esc(it.product_name || '')}</div></div>
          <div class="col-12 col-md-4"><label class="form-label">Almacén origen</label><select class="form-select ship-wh">${whOptionsFromStock(it.source_stock || [], 'source')}</select></div>
          <div class="col-12 col-md-3"><label class="form-label">Cantidad a enviar</label><input class="form-control mono ship-qty" value="${fmt(it.requested_qty)}"></div>
        </div>
      </div>`;
    }
    html += `</form>`;
  }
  if (status === 'ENVIADA') {
    html += `<hr><form id="locReqReceiveForm"><div class="section-title mb-2"><i class="bi bi-box-arrow-in-down text-warning"></i> Confirmar recepción</div>
      <div class="row g-3 mb-3"><div class="col-12 col-md-4"><label class="form-label">Fecha recepción</label><input class="form-control" type="date" name="work_date" value="${operationalDateForForms()}"></div></div>`;
    for (const it of items) {
      html += `<div class="item-row mb-2" data-recv-item="${it.id}">
        <div class="row g-3 align-items-end">
          <div class="col-12 col-md-6"><div class="fw-semibold">${esc(it.sku)}</div><div class="small muted">Enviado: ${fmt(it.shipped_qty || 0)} ${esc(it.uom || '')}</div></div>
          <div class="col-12 col-md-6"><label class="form-label">Almacén destino</label><select class="form-select recv-wh">${whOptionsFromStock(it.dest_allowed_warehouses || [], 'dest')}</select></div>
        </div>
      </div>`;
    }
    html += `</form>`;
  }
  $('locReqDetailBody').innerHTML = html;

  let footer = `<button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button>`;
  if (status === 'PENDIENTE' || status === 'APROBADA') {
    footer += `<button type="button" class="btn btn-outline-danger" onclick="rejectLocReq(${r.id})"><i class="bi bi-x-circle me-1"></i>Rechazar</button>`;
    footer += `<button type="button" class="btn btn-blue-soft" onclick="shipLocReq(${r.id})"><i class="bi bi-truck me-1"></i>Confirmar envío</button>`;
  } else if (status === 'ENVIADA') {
    footer += `<button type="button" class="btn btn-green-soft" onclick="receiveLocReq(${r.id})"><i class="bi bi-check2-circle me-1"></i>Confirmar recepción</button>`;
  }
  $('locReqDetailFooter').innerHTML = footer;
  locReqDetailModal.show();
}

function rejectLocReq(id){
  $('locReqRejectId').value = String(id || 0);
  $('locReqRejectReason').value = '';
  locReqRejectModal.show();
}

async function submitLocReqReject(){
  const id = parseInt($('locReqRejectId').value || '0', 10);
  const reason = String($('locReqRejectReason').value || '').trim();
  if (!reason) {
    $('locReqRejectReason').focus();
    alert('Debes indicar el motivo del rechazo.');
    return;
  }
  const j = await apiPost('locreq_reject', {id, reason});
  if (!j.ok) { alert(j.error || 'No se pudo rechazar'); return; }
  locReqRejectModal.hide();
  locReqDetailModal.hide();
  await loadLocRequests(locReqMode);
}

async function shipLocReq(id){
  const f = document.getElementById('locReqShipForm');
  if (!f) return;
  const items = [...document.querySelectorAll('[data-ship-item]')].map(row => ({
    item_id: parseInt(row.dataset.shipItem || '0', 10),
    source_warehouse_id: parseInt(row.querySelector('.ship-wh')?.value || '0', 10),
    qty: num(row.querySelector('.ship-qty')?.value || 0)
  }));
  const invalidShip = items.some(it => !it.source_warehouse_id || it.qty <= 0);
  if (invalidShip) { alert('Selecciona almacén origen y cantidad válida para todos los productos.'); return; }
  const total = items.reduce((acc, it) => acc + num(it.qty), 0);
  openLocReqConfirmModal({
    title: `Confirmar envío SL-${String(id).padStart(6,'0')}`,
    sub: 'Este movimiento descontará inventario real de la localidad proveedora.',
    body: `<div class="alert alert-warning py-2 mb-3"><strong>Atención:</strong> se generará un OUT real en inventory_moves.</div>
           <div class="confirm-box"><div><strong>Fecha:</strong> ${esc(f.work_date.value || '')}</div><div><strong>Chofer:</strong> ${esc(f.driver_name.value || '-')}</div><div><strong>Unidad:</strong> ${esc(f.vehicle_unit.value || '-')}</div><div><strong>Total piezas:</strong> ${fmt(total)}</div></div>`,
    okText: '<i class="bi bi-truck me-1"></i>Confirmar envío',
    okClass: 'btn btn-blue-soft',
    onConfirm: async () => {
      const j = await apiPost('locreq_ship', {id, work_date:f.work_date.value, driver_name:f.driver_name.value, vehicle_unit:f.vehicle_unit.value, items});
      if (!j.ok) { alert(j.error || 'No se pudo confirmar envío'); return; }
      locReqConfirmModal.hide();
      locReqDetailModal.hide();
      await reloadAll();
      await loadLocRequests('transit');
    }
  });
}

async function receiveLocReq(id){
  const f = document.getElementById('locReqReceiveForm');
  if (!f) return;
  const items = [...document.querySelectorAll('[data-recv-item]')].map(row => ({
    item_id: parseInt(row.dataset.recvItem || '0', 10),
    destination_warehouse_id: parseInt(row.querySelector('.recv-wh')?.value || '0', 10)
  }));
  const invalidRecv = items.some(it => !it.destination_warehouse_id);
  if (invalidRecv) { alert('Selecciona almacén destino para todos los productos. Si no aparece, configura el almacén del producto en Admin.'); return; }
  openLocReqConfirmModal({
    title: `Confirmar recepción SL-${String(id).padStart(6,'0')}`,
    sub: 'Este movimiento aumentará inventario real en la localidad solicitante.',
    body: `<div class="alert alert-success py-2 mb-3"><strong>Confirmación:</strong> se generará un IN real en inventory_moves.</div>
           <div class="confirm-box"><strong>Fecha recepción:</strong> ${esc(f.work_date.value || '')}<br><span class="muted small">Valida que el almacén destino sea correcto antes de continuar.</span></div>`,
    okText: '<i class="bi bi-check2-circle me-1"></i>Confirmar recepción',
    okClass: 'btn btn-green-soft',
    onConfirm: async () => {
      const j = await apiPost('locreq_receive', {id, work_date:f.work_date.value, items});
      if (!j.ok) { alert(j.error || 'No se pudo confirmar recepción'); return; }
      locReqConfirmModal.hide();
      locReqDetailModal.hide();
      await reloadAll();
      await loadLocRequests('history');
    }
  });
}

function showModuleSection(sectionId){
  document.querySelectorAll('.module-section').forEach(x => x.classList.remove('active'));
  document.querySelectorAll('.operation-nav-btn').forEach(x => x.classList.remove('active'));
  const section = document.getElementById(sectionId);
  if (section) section.classList.add('active');
  const nav = document.querySelector(`.operation-nav-btn[data-ops-section="${sectionId}"]`);
  if (nav) nav.classList.add('active');

  // Botón operativo: al revisar rutas de reparto, consulta automáticamente
  // las rutas/órdenes de la fecha seleccionada sin esperar otro clic.
  if (sectionId === 'salesCreatedSection' && typeof loadSalesCreatedOrders === 'function') {
    loadSalesCreatedOrders();
  }
}

function renderSalesPager(){
  const box = $('salesPager');
  if (!box) return;

  if (salesPages <= 1) {
    box.innerHTML = salesTotal > 0
      ? `<div class="small muted">Mostrando ${salesRows.length} de ${salesTotal} pedidos.</div>`
      : '';
    return;
  }

  let html = `
    <div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
      <div class="small muted">
        Página ${salesPage} de ${salesPages} · Total pedidos: ${salesTotal}
      </div>
      <div class="d-flex gap-2 flex-wrap">
  `;

  html += `
    <button class="btn toolbar-btn btn-sm" type="button"
      ${salesPage <= 1 ? 'disabled' : ''}
      onclick="loadSalesOrders(${salesPage - 1})">
      Anterior
    </button>
  `;

  let dotsLeft = false;
  let dotsRight = false;
  for (let p = 1; p <= salesPages; p++) {
    const show = p === 1 || p === salesPages || Math.abs(p - salesPage) <= 1;
    if (show) {
      html += `
        <button class="btn btn-sm ${p === salesPage ? 'btn-blue-soft' : 'toolbar-btn'}"
          type="button"
          onclick="loadSalesOrders(${p})">
          ${p}
        </button>
      `;
    } else if (p < salesPage && !dotsLeft) {
      html += `<span class="px-1 align-self-center">...</span>`;
      dotsLeft = true;
    } else if (p > salesPage && !dotsRight) {
      html += `<span class="px-1 align-self-center">...</span>`;
      dotsRight = true;
    }
  }

  html += `
    <button class="btn toolbar-btn btn-sm" type="button"
      ${salesPage >= salesPages ? 'disabled' : ''}
      onclick="loadSalesOrders(${salesPage + 1})">
      Siguiente
    </button>
  `;

  html += `</div></div>`;
  box.innerHTML = html;
}

async function loadCatalogs(){
  const [p, c, w, m] = await Promise.all([
    apiGet('products'),
    apiGet('channels'),
    apiGet('warehouses'),
    apiGet('merma_concepts')
  ]);

  products = p.ok ? (p.rows || []) : [];
  channels = c.ok ? (c.rows || []) : [];
  warehouses = w.ok ? (w.rows || []) : [];
  mermaConcepts = m.ok ? (m.rows || []) : [];

  const bar = IS_HIELO_BARRA_SCOPE ? products.find(x => String(x.sku).toUpperCase() === 'BARRA 150 KILOS') : null;
  barProductId = bar ? parseInt(bar.id, 10) : 0;
}

async function loadSalesOrders(page = 1){
  if (!IS_HIELO_BARRA_SCOPE) {
    salesRows = [];
    salesTotal = 0;
    if ($('countVH')) $('countVH').textContent = '0';
    return;
  }

  const j = await apiGet('vh_authorized', {
    page,
    pageSize: salesPageSize
  });

  if (!j.ok) {
    alert(j.error || 'Error al cargar pedidos de VentasHielo');
    return;
  }

  salesRows = j.rows || [];
  salesPage = parseInt(j.page || 1, 10);
  salesPageSize = parseInt(j.pageSize || 10, 10);
  salesPages = parseInt(j.pages || 1, 10);
  salesTotal = parseInt(j.total || 0, 10);

  renderSalesOrders();
  renderSalesPager();
}

async function loadInventoryForSelectedDates(){
  initHistoryDates();
  const dateFrom = $('histDateFrom')?.value || getMonthRange().from;
  const dateTo = $('histDateTo')?.value || getMonthRange().to;
  const inv = await apiGet('inventory_total', {date_from: dateFrom, date_to: dateTo});
  if (!inv.ok) {
    alert(inv.error || 'Error al cargar inventario');
    return false;
  }

  invRows = inv.rows || [];
  barProductId = inv.bar_product_id || barProductId;
  renderInventory();
  return true;
}

async function reloadAll(){
  const invOk = await loadInventoryForSelectedDates();
  if (!invOk) return;

  const mv = await apiGet('moves_list_total');
  if (!mv.ok) {
    alert(mv.error || 'Error al cargar movimientos');
    return;
  }
  const moveRows = mv.rows || [];
  renderMoves(moveRows);
  renderReturns(moveRows);
  await loadMovementSummary();
  renderProductCards();

  await loadSalesOrders(salesPage);
  await loadSalesCreatedOrders();
  await loadWarehouseMermas();
  await loadLocRequests(locReqMode);
  updateModuleCounts(moveRows, mermaRows || []);
  await loadDispatchMailbox();
}

function channelOptions(){
  return channels.map(c => `<option value="${c.id}">${esc(c.name)} (${esc(c.channel_type)})</option>`).join('');
}
function productOptions(){
  return products.map(p => `<option value="${p.id}">${esc(p.sku)} · ${esc(p.name)}</option>`).join('');
}
function warehouseOptions(){
  return warehouses.map(w => `<option value="${w.id}">${esc(w.code)} · ${esc(w.name)}</option>`).join('');
}


function mermaConceptOptions(){
  return mermaConcepts.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('');
}

function renderWarehouseMermas(rows){
  const tb = $('mermaBody');
  if (!tb) return;
  tb.innerHTML = '';

  if (!rows.length) {
    $('mermaEmptyHint').textContent = 'Sin mermas de almacén registradas.';
    tb.innerHTML = `<tr><td colspan="6" class="text-center muted py-4">Sin mermas registradas.</td></tr>`;
    return;
  }

  $('mermaEmptyHint').textContent = '';
  for (const r of rows) {
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td><div class="mono fw-semibold">${esc(r.work_date || '')}</div><div class="small muted mono">${esc(String(r.created_at || '').substring(11,19))}</div></td>
        <td>
          <div class="fw-semibold" >${esc(r.sku || '')}</div>
          <div class="small" >${esc(r.product_name || '')}</div>
        </td>
        <td>
          <div class="fw-semibold">${esc(r.warehouse_code || '')}</div>
          <div class="small" >${esc(r.warehouse_name || '')}</div>
        </td>
        <td>${esc(r.concept_name || '')}</td>
        <td class="text-end mono fw-bold">${fmt(r.qty)}</td>
        <td class="small" >${esc(r.notes || '')}</td>
      </tr>
    `);
  }
}

async function loadWarehouseMermas(page=mermaPage){
  initHistoryDates();
  mermaPage = page || 1;
  const dateFrom = $('histDateFrom')?.value || getMonthRange().from;
  const dateTo = $('histDateTo')?.value || getMonthRange().to;
  const j = await apiGet('warehouse_merma_list', {date_from: dateFrom, date_to: dateTo, page: mermaPage, per_page: mermaPageSize});
  if (!j.ok) {
    alert(j.error || 'Error al cargar mermas de almacén');
    return;
  }
  mermaRows = j.rows || [];
  mermaPagination = j.pagination || {};
  renderWarehouseMermas(mermaRows);
  renderMermaPager(mermaPagination);
}

function renderMermaPager(pagination){
  const pg = $('mermaPager');
  if (!pg) return;
  const page = Number(pagination.page || 1);
  const totalPages = Number(pagination.total_pages || 1);
  const totalRows = Number(pagination.total_rows || 0);
  pg.innerHTML = `
    <div class="small muted">Página ${page} de ${totalPages} · Registros: ${totalRows}</div>
    <div class="d-flex gap-2">
      <button class="btn toolbar-btn btn-sm" type="button" id="btnMermaPrev" ${page <= 1 ? 'disabled' : ''}>Anterior</button>
      <button class="btn toolbar-btn btn-sm" type="button" id="btnMermaNext" ${page >= totalPages ? 'disabled' : ''}>Siguiente</button>
    </div>
  `;
  if ($('btnMermaPrev')) $('btnMermaPrev').addEventListener('click', () => loadWarehouseMermas(page - 1));
  if ($('btnMermaNext')) $('btnMermaNext').addEventListener('click', () => loadWarehouseMermas(page + 1));
}

async function refreshWarehouseStockHint(){
  const productId = parseInt($('mProduct')?.value || '0', 10);
  const warehouseId = parseInt($('mWarehouse')?.value || '0', 10);
  const hint = $('mWarehouseStockHint');
  if (!hint) return;
  if (productId <= 0 || warehouseId <= 0) {
    hint.textContent = 'Disponible en almacén: 0';
    return;
  }
  const j = await apiGet('warehouse_stock', {product_id: productId, warehouse_id: warehouseId});
  if (!j.ok) {
    hint.textContent = 'Disponible en almacén: N/D';
    return;
  }
  hint.textContent = `Disponible en almacén: ${fmt(j.available || 0)}`;
}

function openMermaModal(){
  $('mermaForm').reset();
  $('mProduct').innerHTML = productOptions();
  $('mWarehouse').innerHTML = warehouseOptions();
  $('mConcept').innerHTML = mermaConceptOptions();

  $('mermaForm').querySelector('[name="work_date"]').value = operationalDateForForms();

  refreshWarehouseStockHint();
  mermaModal.show();
}

function collectMermaForm(){
  const fd = new FormData($('mermaForm'));
  const payload = Object.fromEntries(fd.entries());
  payload.product_id = parseInt(payload.product_id || '0', 10);
  payload.warehouse_id = parseInt(payload.warehouse_id || '0', 10);
  payload.concept_id = parseInt(payload.concept_id || '0', 10);
  payload.qty = parseFloat(String(payload.qty || '0').replace(',', '.')) || 0;
  return payload;
}

async function saveWarehouseMerma(){
  const payload = collectMermaForm();
  const j = await apiPost('warehouse_merma_save', payload);
  if (!j.ok) {
    alert(j.error || 'No se pudo guardar la merma');
    return;
  }
  mermaModal.hide();
  await reloadAll();
  alert('Merma registrada correctamente.');
}


function renderReturns(moveRows = []){
  const tb = $('returnBody');
  if (!tb) return;
  const rows = (moveRows || []).filter(r => String(r.ref_type || '').toUpperCase() === 'WAREHOUSE_RETURN');
  tb.innerHTML = '';
  if (!rows.length) {
    if ($('returnEmptyHint')) $('returnEmptyHint').textContent = 'Sin devoluciones registradas.';
    tb.innerHTML = `<tr><td colspan="7" class="text-center muted py-4">Sin devoluciones registradas.</td></tr>`;
    return;
  }
  if ($('returnEmptyHint')) $('returnEmptyHint').textContent = '';
  for (const r of rows) {
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td><div class="mono fw-semibold">${esc(r.work_date || '')}</div><div class="small muted mono">${esc(String(r.created_at || '').substring(11,19))}</div></td>
        <td>
          <div class="fw-semibold">${esc(r.sku || '')}</div>
          <div class="small">${esc(r.product_name || '')}</div>
        </td>
        <td>
          <div class="fw-semibold">${esc(r.warehouse_code || '')}</div>
          <div class="small">${esc(r.warehouse_name || '')}</div>
        </td>
        <td><span class="badge text-bg-success">IN devolución</span></td>
        <td class="small">${esc(r.notes || '')}</td>
        <td class="small">${esc(r.user_name || r.username || '')}</td>
        <td class="text-end mono fw-bold">${fmt(r.qty)}</td>
      </tr>
    `);
  }
}

function buildManualStockInSummary(data, confirmed = false){
  return `
    <div class="mb-3">
      <div class="small muted">${confirmed ? 'Movimiento aplicado' : 'Resumen antes de confirmar'}</div>
      <div class="fw-bold">${esc(data.product_sku || '')} · ${esc(data.product_name || '')}</div>
      <div class="small muted">Almacén: <span class="mono fw-semibold">${esc(data.warehouse_code || '')}</span> · ${esc(data.warehouse_name || '')}</div>
      <div class="small muted">Fecha operativa: <span class="mono">${esc(data.work_date || '')}</span></div>
      ${data.line_name || data.provider ? `<div class="small muted">Línea: <b>${esc(data.line_name || '—')}</b> · Proveedor: <b>${esc(data.provider || '—')}</b></div>` : ''}
      ${data.route_label ? `<div class="small muted">Ruta: <b>${esc(data.route_label)}</b></div>` : ''}
    </div>

    <div class="table-wrap mb-3">
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead>
            <tr>
              <th>Embalaje</th>
              <th class="text-end">Rack's/Tarimas</th>
              <th class="text-end">Paq/Rack</th>
              <th class="text-end">Paq sueltos</th>
              <th class="text-end">Total paquetes</th>
              <th class="text-end">Pz/Paq</th>
              <th class="text-end">Pz sueltas</th>
              <th class="text-end">Total piezas</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>${esc(data.packaging_name || '')}</td>
              <td class="text-end mono fw-bold">${fmt(data.rack_qty || 0)}</td>
              <td class="text-end mono">${fmt(data.packages_per_rack || 0)}</td>
              <td class="text-end mono">${fmt(data.loose_packages || 0)}</td>
              <td class="text-end mono fw-bold">${fmt(data.total_packages || 0)}</td>
              <td class="text-end mono">${fmt(data.pieces_per_package || 0)}</td>
              <td class="text-end mono">${fmt(data.loose_pieces || 0)}</td>
              <td class="text-end mono fw-bold text-success">${fmt(data.qty || 0)}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div class="table-wrap mb-3">
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead>
            <tr>
              <th>Almacén destino</th>
              <th class="text-end">Cantidad anterior</th>
              <th class="text-end">Entrada IN</th>
              <th class="text-end">Cantidad actual</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>
                <div class="mono fw-bold">${esc(data.warehouse_code || '')}</div>
                <div class="small muted">${esc(data.warehouse_name || '')}</div>
              </td>
              <td class="text-end mono fw-bold">${fmt(data.stock_before || 0)}</td>
              <td class="text-end mono fw-bold text-success">+${fmt(data.qty || 0)}</td>
              <td class="text-end mono fw-bold text-primary">${fmt(data.stock_after || 0)}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    ${data.notes ? `<div class="item-row"><div class="muted small">Observación</div><div>${esc(data.notes)}</div></div>` : ''}
  `;
}

async function loadManualStockInWarehouses(){
  if (!CAN_ADD_EXISTENCE) return;
  const productId = parseInt($('msiProduct')?.value || '0', 10);
  const sel = $('msiWarehouse');
  if (!sel) return;
  sel.innerHTML = '';

  if (productId <= 0) {
    sel.innerHTML = '<option value="">Selecciona artículo</option>';
    $('msiWarehouseHint').textContent = 'Selecciona artículo para consultar almacenes.';
    return;
  }

  const j = await apiGet('manual_stock_in_warehouses', {product_id: productId});
  if (!j.ok) {
    sel.innerHTML = '<option value="">Sin almacenes disponibles</option>';
    $('msiWarehouseHint').textContent = j.error || 'No se pudieron consultar almacenes.';
    return;
  }

  const rows = j.rows || [];
  sel.innerHTML = rows.length
    ? rows.map(w => `<option value="${esc(w.warehouse_id)}">${esc(w.code)} · ${esc(w.name)} · actual ${fmt(w.qty || 0)}</option>`).join('')
    : '<option value="">Sin almacenes operativos configurados</option>';

  $('msiWarehouseHint').textContent = rows.length
    ? 'Almacenes activos/configurados de la planta actual.'
    : 'Configura el almacén del artículo antes de registrar existencia.';
}


async function loadManualStockInRoutes(){
  if (!CAN_ADD_EXISTENCE) return;
  const sel = $('msiRoute');
  if (!sel) return;
  sel.innerHTML = '<option value="">Sin ruta / interno</option>';
  const j = await apiGet('manual_stock_in_routes');
  if (!j.ok) return;
  const rows = j.rows || [];
  sel.innerHTML = '<option value="">Sin ruta / interno</option>' + rows.map(r => {
    const label = `${r.route_code || ('Ruta ' + r.route_no)} · ${r.route_name || ''}`.trim();
    return `<option value="${Number(r.route_no)}" data-label="${esc(label)}">${esc(label)}</option>`;
  }).join('');
}

async function loadManualStockInPackaging(){
  if (!CAN_ADD_EXISTENCE) return;
  const productId = parseInt($('msiProduct')?.value || '0', 10);
  const sel = $('msiPackaging');
  if (!sel) return;

  if (productId <= 0) {
    sel.innerHTML = '<option value="">Selecciona artículo</option>';
    updateManualStockInCalc();
    return;
  }

  const j = await apiGet('manual_stock_in_packaging_options', {product_id: productId});
  if (!j.ok || !(j.rows || []).length) {
    sel.innerHTML = '<option value="">Sin embalaje configurado</option>';
    updateManualStockInCalc();
    return;
  }

  sel.innerHTML = (j.rows || []).map(p => `
    <option value="${Number(p.id || 0)}"
            data-packages="${Number(p.packages_per_pallet || 0)}"
            data-pieces="${Number(p.pieces_per_package || 0)}"
            data-name="${esc(p.packaging_name || p.packaging_type || '')}">
      ${esc(p.packaging_name || p.packaging_type || '')} · ${fmt(p.packages_per_pallet || 0)} paq/rack · ${fmt(p.pieces_per_package || 0)} pz/paq${Number(p.legacy_fallback || 0) === 1 ? ' · principal' : ''}
    </option>
  `).join('');
  updateManualStockInCalc();
}

function manualStockInSelectedPackaging(){
  const opt = $('msiPackaging')?.selectedOptions?.[0];
  if (!opt) return null;
  return {
    id: parseInt(opt.value || '0', 10),
    packages_per_rack: parseInt(opt.dataset.packages || '0', 10),
    pieces_per_package: parseInt(opt.dataset.pieces || '0', 10),
    name: opt.dataset.name || opt.textContent || ''
  };
}

function updateManualStockInCalc(){
  const p = manualStockInSelectedPackaging();
  const rackQty = parseInt($('msiRackQty')?.value || '0', 10) || 0;
  const loosePackages = parseInt($('msiLoosePackages')?.value || '0', 10) || 0;
  const loosePieces = parseInt($('msiLoosePieces')?.value || '0', 10) || 0;
  const packagesPerRack = p ? Number(p.packages_per_rack || 0) : 0;
  const piecesPerPackage = p ? Number(p.pieces_per_package || 0) : 0;
  const totalPackages = (rackQty * packagesPerRack) + loosePackages;
  const totalPieces = (totalPackages * piecesPerPackage) + loosePieces;

  if ($('msiCalcPreview')) {
    $('msiCalcPreview').value = `${fmt(totalPackages)} paquetes · ${fmt(totalPieces)} piezas`;
  }
  if ($('msiTotalPieces')) {
    $('msiTotalPieces').value = fmt(totalPieces);
  }
}


async function openManualStockInModal(){
  if (!CAN_ADD_EXISTENCE || !manualStockInModal) return;
  $('manualStockInForm').reset();
  $('msiWorkDate').value = operationalDateForForms();
  $('msiProduct').innerHTML = productOptions();
  if ($('msiRackQty')) $('msiRackQty').value = '0';
  if ($('msiLoosePackages')) $('msiLoosePackages').value = '0';
  if ($('msiLoosePieces')) $('msiLoosePieces').value = '0';
  await Promise.all([loadManualStockInWarehouses(), loadManualStockInPackaging(), loadManualStockInRoutes()]);
  updateManualStockInCalc();
  manualStockInModal.show();
}

function collectManualStockInPayload(){
  return {
    product_id: parseInt($('msiProduct')?.value || '0', 10),
    warehouse_id: parseInt($('msiWarehouse')?.value || '0', 10),
    packaging_option_id: parseInt($('msiPackaging')?.value || '0', 10),
    rack_qty: parseFloat(String($('msiRackQty')?.value || '0').replace(',', '.')) || 0,
    loose_packages: parseFloat(String($('msiLoosePackages')?.value || '0').replace(',', '.')) || 0,
    loose_pieces: parseFloat(String($('msiLoosePieces')?.value || '0').replace(',', '.')) || 0,
    line_name: String($('msiLineName')?.value || '').trim(),
    provider: String($('msiProvider')?.value || '').trim(),
    route_no: parseInt($('msiRoute')?.value || '0', 10),
    route_label: $('msiRoute')?.selectedOptions?.[0]?.dataset?.label || '',
    notes: String($('msiNotes')?.value || '').trim()
  };
}

async function previewManualStockIn(){
  const payload = collectManualStockInPayload();

  if (!payload.product_id || !payload.warehouse_id) {
    alert('Selecciona artículo y almacén destino.');
    return;
  }
  const numericValues = [payload.rack_qty, payload.loose_packages, payload.loose_pieces];
  if (numericValues.some(v => v < 0 || !Number.isInteger(v))) {
    alert('Rack/tarimas, paquetes sueltos y piezas sueltas deben ser números enteros sin negativos.');
    return;
  }
  if ((payload.rack_qty + payload.loose_packages + payload.loose_pieces) <= 0) {
    alert('Captura al menos rack/tarimas, paquetes sueltos o piezas sueltas.');
    return;
  }

  const j = await apiPost('manual_stock_in_preview', payload);
  if (!j.ok) {
    alert(j.error || 'No se pudo validar la entrada de existencias.');
    return;
  }

  pendingManualStockInPayload = payload;
  $('manualStockInConfirmSummary').innerHTML = buildManualStockInSummary(j, false);
  manualStockInModal.hide();
  manualStockInConfirmModal.show();
}

async function confirmManualStockIn(){
  if (!pendingManualStockInPayload) return;

  const btn = $('btnConfirmManualStockIn');
  const originalHtml = btn.innerHTML;
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Registrando...';

  try {
    const j = await apiPost('manual_stock_in_save', pendingManualStockInPayload);
    if (!j.ok) {
      alert(j.error || 'No se pudo agregar la existencia.');
      return;
    }

    pendingManualStockInPayload = null;
    manualStockInConfirmModal.hide();
    $('manualStockInResultSummary').innerHTML = buildManualStockInSummary(j, true);
    await reloadAll();
    manualStockInResultModal.show();
  } finally {
    btn.disabled = false;
    btn.innerHTML = originalHtml;
  }
}

async function loadReturnWarehouses(){
  const productId = parseInt($('rProduct')?.value || '0', 10);
  const sel = $('rWarehouse');
  if (!sel) return;
  sel.innerHTML = '';
  if (productId <= 0) {
    sel.innerHTML = `<option value="">Selecciona producto</option>`;
    return;
  }
  const j = await apiGet('return_warehouses', {product_id: productId});
  if (!j.ok) {
    sel.innerHTML = `<option value="">Error al cargar almacenes</option>`;
    if ($('rWarehouseStockHint')) $('rWarehouseStockHint').textContent = j.error || 'No se pudieron cargar almacenes.';
    return;
  }
  const rows = j.rows || [];
  if (!rows.length) {
    sel.innerHTML = `<option value="">Sin almacén configurado</option>`;
    if ($('rWarehouseStockHint')) $('rWarehouseStockHint').textContent = 'Configura almacén de entrada en Admin > Productos.';
    return;
  }
  sel.innerHTML = rows.map(w => `<option value="${w.warehouse_id}">${esc(w.code)} · ${esc(w.name || '')} · disponible ${fmt(w.qty || 0)}</option>`).join('');
  await refreshReturnWarehouseStockHint();
}

async function refreshReturnWarehouseStockHint(){
  const productId = parseInt($('rProduct')?.value || '0', 10);
  const warehouseId = parseInt($('rWarehouse')?.value || '0', 10);
  const hint = $('rWarehouseStockHint');
  if (!hint) return;
  if (productId <= 0 || warehouseId <= 0) {
    hint.textContent = 'Disponible actual: 0';
    return;
  }
  const j = await apiGet('warehouse_stock', {product_id: productId, warehouse_id: warehouseId});
  if (!j.ok) {
    hint.textContent = 'Disponible actual: N/D';
    return;
  }
  hint.textContent = `Disponible actual: ${fmt(j.available || 0)}`;
}

async function openReturnModal(){
  $('returnForm').reset();
  $('rProduct').innerHTML = productOptions();

  $('returnForm').querySelector('[name="work_date"]').value = operationalDateForForms();

  await loadReturnWarehouses();
  returnModal.show();
}

function collectReturnForm(){
  const fd = new FormData($('returnForm'));
  const payload = Object.fromEntries(fd.entries());
  payload.product_id = parseInt(payload.product_id || '0', 10);
  payload.warehouse_id = parseInt(payload.warehouse_id || '0', 10);
  payload.qty = parseFloat(String(payload.qty || '0').replace(',', '.')) || 0;
  return payload;
}

function buildWarehouseReturnStockSummary(data, confirmed = false){
  return `
    <div class="mb-3">
      <div class="small muted">${confirmed ? 'Movimiento aplicado' : 'Resumen del movimiento a registrar'}</div>
      <div class="fw-bold">${esc(data.product_sku || '')} · ${esc(data.product_name || '')}</div>
      <div class="small muted">Destino: <span class="mono fw-semibold">${esc(data.warehouse_code || '')}</span> · ${esc(data.warehouse_name || '')}</div>
    </div>
    <div class="table-wrap mb-3">
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead>
            <tr>
              <th>Almacén destino</th>
              <th class="text-end">Antes</th>
              <th class="text-end">Entrada devolución</th>
              <th class="text-end">Después</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td><div class="mono fw-bold">${esc(data.warehouse_code || '')}</div><div class="small muted">${esc(data.warehouse_name || '')}</div></td>
              <td class="text-end mono fw-bold">${fmt(data.stock_before || 0)}</td>
              <td class="text-end mono fw-bold text-success">+${fmt(data.qty || 0)}</td>
              <td class="text-end mono fw-bold text-primary">${fmt(data.stock_after || 0)}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    <div class="row g-2 small">
      <div class="col-12 col-md-6"><div class="item-row"><div class="muted">Referencia salida</div><div class="mono fw-semibold">${esc(data.reference || '')}</div></div></div>
      <div class="col-12 col-md-6"><div class="item-row"><div class="muted">Motivo</div><div class="fw-semibold">${esc(data.reason || '')}</div></div></div>
    </div>
  `;
}

async function saveWarehouseReturn(){
  const payload = collectReturnForm();
  if (!payload.reference || !String(payload.reference).trim()) {
    alert('Indica la referencia o número de salida original.');
    return;
  }
  if (!payload.product_id || !payload.warehouse_id || payload.qty <= 0) {
    alert('Producto, almacén destino y cantidad son obligatorios.');
    return;
  }
  if (!payload.reason || !String(payload.reason).trim()) {
    alert('Indica el motivo de la devolución.');
    return;
  }

  // Solo consulta el impacto; todavía no se registra movimiento.
  const preview = await apiPost('warehouse_return_preview', payload);
  if (!preview.ok) {
    alert(preview.error || 'No se pudo validar la devolución');
    return;
  }

  pendingWarehouseReturnPayload = payload;
  $('returnConfirmSummary').innerHTML = buildWarehouseReturnStockSummary(preview, false);
  returnModal.hide();
  returnConfirmModal.show();
}

async function confirmWarehouseReturn(){
  if (!pendingWarehouseReturnPayload) return;

  const btn = $('btnConfirmWarehouseReturn');
  const originalHtml = btn.innerHTML;
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Registrando...';

  try {
    const j = await apiPost('warehouse_return_save', pendingWarehouseReturnPayload);
    if (!j.ok) {
      alert(j.error || 'No se pudo guardar la devolución');
      return;
    }

    pendingWarehouseReturnPayload = null;
    returnConfirmModal.hide();
    $('returnResultSummary').innerHTML = buildWarehouseReturnStockSummary(j, true);
    await reloadAll();
    returnResultModal.show();
  } finally {
    btn.disabled = false;
    btn.innerHTML = originalHtml;
  }
}


function manualOutCustomerLabel(c){
  const code = c.customer_code ? `${c.customer_code} · ` : '';
  const name = c.business_name || c.contact_name || ('Cliente #' + c.id);
  const social = c.contact_name && c.contact_name !== c.business_name ? ` · ${c.contact_name}` : '';
  return `${code}${name}${social}`;
}

async function loadManualOutCustomers(){
  const sel = $('fCustomer');
  if (!sel) return;
  sel.innerHTML = '<option value="">Cargando clientes...</option>';
  const j = await apiGet('botella_manual_customers');
  manualOutCustomers = j.ok ? (j.rows || []) : [];
  if (!manualOutCustomers.length) {
    sel.innerHTML = '<option value="">Sin clientes activos para esta localidad</option>';
    return;
  }
  sel.innerHTML = '<option value="">Selecciona cliente registrado</option>' + manualOutCustomers.map(c => {
    const loc = c.location_name ? ` · ${c.location_name}${c.location_code ? ' (' + c.location_code + ')' : ''}` : '';
    return `<option value="${Number(c.id || 0)}">${esc(manualOutCustomerLabel(c) + loc)}</option>`;
  }).join('');
}

async function loadManualOutRoutes(){
  const sel = $('fRouteNo');
  if (!sel) return;
  sel.innerHTML = '<option value="">Cargando rutas...</option>';
  const j = await apiGet('botella_manual_routes');
  const rawRoutes = j.ok ? (j.rows || []) : [];
  manualOutRoutes = rawRoutes.filter(r => {
    const no = Number(r.route_no || 0);
    const code = String(r.route_code || '').toUpperCase();
    const name = String(r.route_name || '').toUpperCase();
    return no !== 1 && !code.includes('VENTA') && !name.includes('VENTA') && !name.includes('CARGA DE CLIENTES');
  });
  if (!manualOutRoutes.length) {
    sel.innerHTML = '<option value="">Sin rutas de reparto activas en Admin</option>';
    renderRouteStockPanel(null);
    return;
  }
  sel.innerHTML = '<option value="">Selecciona ruta de reparto</option>' + manualOutRoutes.map(r => {
    const label = `${r.route_code || ('Ruta ' + r.route_no)} · ${r.route_name || ''}`.trim();
    return `<option value="${Number(r.route_no || 0)}">${esc(label)}</option>`;
  }).join('');
  renderRouteStockPanel(null);
}

function renderRouteStockPanel(data){
  const panel = $('routeStockPanel');
  if (!panel) return;
  const list = $('routeStockList');
  manualRouteStockRows = [];
  manualRouteStockMap = new Map();

  if (!data || !data.ok) {
    panel.classList.remove('d-none');
    if ($('routeStockSubtitle')) $('routeStockSubtitle').textContent = 'Selecciona una ruta para revisar producto cargado actualmente.';
    if ($('routeStockPieces')) $('routeStockPieces').textContent = '0';
    if ($('routeStockPackages')) $('routeStockPackages').textContent = '0';
    if ($('routeStockPallets')) $('routeStockPallets').textContent = '0';
    if (list) list.innerHTML = '<div class="route-stock-empty">Sin ruta seleccionada.</div>';
    updateAllManualRowPreviews();
    return;
  }

  const rows = data.rows || [];
  manualRouteStockRows = rows;
  manualRouteStockMap = new Map(rows.map(r => [Number(r.product_id || 0), r]));
  const wh = data.warehouse_code || '';
  const totals = data.totals || {};
  panel.classList.remove('d-none');
  if ($('routeStockSubtitle')) $('routeStockSubtitle').textContent = `${wh} · inventario móvil actual antes de agregar nueva carga.`;
  if ($('routeStockPieces')) $('routeStockPieces').textContent = fmt(totals.pieces || 0);
  if ($('routeStockPackages')) $('routeStockPackages').textContent = fmt(totals.packages || 0);
  if ($('routeStockPallets')) $('routeStockPallets').textContent = fmt(totals.pallets || 0);

  if (!list) return;
  if (!rows.length) {
    list.innerHTML = '<div class="route-stock-empty">Esta ruta no trae existencia cargada actualmente. La carga nueva iniciará el saldo de la ruta.</div>';
  } else {
    list.innerHTML = rows.map(r => `
      <div class="route-stock-item">
        <div class="sku">${esc(r.sku || '')}</div>
        <div class="name">${esc(r.name || '')}</div>
        <div class="nums">
          <span>${fmt(r.current_qty || 0)} pz</span>
          <span>${fmt(r.packages || 0)} paq</span>
          <span>${fmt(r.pallets || 0)} tar</span>
        </div>
      </div>
    `).join('');
  }
  updateAllManualRowPreviews();
}

async function loadSelectedRouteStock(){
  const type = String($('fOrderType')?.value || 'RUTA').toUpperCase();
  const routeNo = parseInt($('fRouteNo')?.value || '0', 10);
  if (type !== 'RUTA' || routeNo <= 0) {
    renderRouteStockPanel(null);
    return;
  }
  const panel = $('routeStockPanel');
  const list = $('routeStockList');
  if (panel) panel.classList.remove('d-none');
  if (list) list.innerHTML = '<div class="route-stock-empty">Consultando existencia de ruta...</div>';
  const workDate = $('editForm')?.querySelector('[name="work_date"]')?.value || operationalDateForForms();
  const j = await apiGet('botella_route_current_stock', {route_no: routeNo, work_date: workDate});
  if (!j.ok) {
    if (list) list.innerHTML = `<div class="route-stock-empty">${esc(j.error || 'No se pudo cargar existencia de ruta.')}</div>`;
    manualRouteStockRows = [];
    manualRouteStockMap = new Map();
    updateAllManualRowPreviews();
    return;
  }
  renderRouteStockPanel(j);
}

function updateAllManualRowPreviews(){
  document.querySelectorAll('#itemsWrap .item-row').forEach(row => updateManualRowPreview(row));
}

function updateManualOutOrderType(){
  const type = String($('fOrderType')?.value || 'RUTA').toUpperCase();
  const isClient = type === 'CLIENTE';

  // OSV: salida urgente a cliente. Debe comportarse como Ventas Botella:
  // solicita OC + cliente + punto de entrega/dirección, sin ruta.
  document.querySelectorAll('.js-osv-field').forEach(el => el.classList.toggle('d-none', !isClient));

  // OSA: carga a ruta. No solicita OC ni cliente; solo ruta + artículos.
  document.querySelectorAll('.js-route-field').forEach(el => el.classList.toggle('d-none', isClient));

  if ($('fRouteNo')) {
    $('fRouteNo').required = !isClient;
    $('fRouteNo').disabled = isClient;
    if (isClient) $('fRouteNo').value = '';
  }
  if ($('fRouteLabel') && isClient) $('fRouteLabel').value = '';

  if ($('fReference')) {
    $('fReference').required = isClient;
    $('fReference').disabled = !isClient;
    if (!isClient) $('fReference').value = '';
  }
  if ($('fCustomer')) {
    $('fCustomer').required = isClient;
    $('fCustomer').disabled = !isClient;
    if (!isClient) $('fCustomer').value = '';
  }
  if ($('fAddressId')) {
    $('fAddressId').required = isClient;
    $('fAddressId').disabled = !isClient;
    if (!isClient) $('fAddressId').value = '';
  }
  if (!isClient) {
    ['fDeliveryAddress','fCustomerCode','fCustomerBusinessName','fCustomerName','fCustomerLocationLabel'].forEach(id => { if ($(id)) $(id).value = ''; });
  }

  if ($('btnSaveManualOut')) $('btnSaveManualOut').innerHTML = isClient
    ? '<i class="bi bi-save me-1"></i>Crear OSV'
    : '<i class="bi bi-save me-1"></i>Crear OSA';

  const stockPanel = $('routeStockPanel');
  if (stockPanel) stockPanel.classList.toggle('d-none', isClient);
  if (isClient) {
    manualRouteStockRows = [];
    manualRouteStockMap = new Map();
  } else {
    loadSelectedRouteStock();
  }
}

async function loadManualOutAddresses(customerId){
  const sel = $('fAddressId');
  if (!sel) return;
  manualOutAddresses = [];
  if ($('fDeliveryAddress')) $('fDeliveryAddress').value = '';
  if (!customerId) {
    sel.innerHTML = '<option value="">Selecciona cliente</option>';
    return;
  }

  sel.innerHTML = '<option value="">Cargando puntos...</option>';
  const j = await apiGet('botella_manual_customer_addresses', {customer_id: customerId});
  manualOutAddresses = j.ok ? (j.rows || []) : [];
  if (!manualOutAddresses.length) {
    sel.innerHTML = '<option value="">Sin puntos de entrega activos</option>';
    return;
  }
  sel.innerHTML = '<option value="">Selecciona punto de entrega</option>' + manualOutAddresses.map(a => {
    const loc = a.location_name ? ` · ${a.location_name}${a.location_code ? ' (' + a.location_code + ')' : ''}` : '';
    const def = Number(a.is_default || 0) === 1 ? ' · principal' : '';
    return `<option value="${Number(a.id || 0)}">${esc((a.label || 'Punto de entrega') + loc + def)}</option>`;
  }).join('');
  const defaultAddr = manualOutAddresses.find(a => Number(a.is_default || 0) === 1) || manualOutAddresses[0];
  if (defaultAddr) {
    sel.value = String(defaultAddr.id || 0);
    applyManualOutAddress();
  }
}

function applyManualOutCustomer(){
  const id = parseInt($('fCustomer')?.value || '0', 10);
  const c = manualOutCustomers.find(x => Number(x.id || 0) === id) || null;
  $('fCustomerCode').value = c ? (c.customer_code || '') : '';
  $('fCustomerBusinessName').value = c ? (c.contact_name || '') : '';
  $('fCustomerName').value = c ? (c.business_name || '') : '';
  $('fCustomerLocationLabel').value = c ? `${c.location_name || CURRENT_LOCATION_NAME}${c.location_code ? ' (' + c.location_code + ')' : ''}` : '';
  $('fDeliveryAddress').value = c ? (c.address || '') : '';
  loadManualOutAddresses(id);
}

function applyManualOutAddress(){
  const id = parseInt($('fAddressId')?.value || '0', 10);
  const a = manualOutAddresses.find(x => Number(x.id || 0) === id) || null;
  $('fDeliveryAddress').value = a ? (a.address || '') : '';
}

function applyManualOutRoute(){
  const routeNo = parseInt($('fRouteNo')?.value || '0', 10);
  const r = manualOutRoutes.find(x => Number(x.route_no || 0) === routeNo) || null;
  if (!r) {
    $('fRouteLabel').value = '';
    if ($('fRouteDisplay')) $('fRouteDisplay').value = '';
    renderRouteStockPanel(null);
    return;
  }
  const label = `${r.route_code || ('Ruta ' + r.route_no)} · ${r.route_name || ''}`.trim();
  $('fRouteLabel').value = label;
  if ($('fRouteDisplay')) $('fRouteDisplay').value = label;
  const vehicle = $('fVehicleUnit');
  if (vehicle && !vehicle.value && r.vehicle_unit) vehicle.value = r.vehicle_unit;
  loadSelectedRouteStock();
}

function manualSelectedPackaging(row){
  const id = parseInt(row.querySelector('[name="item_packaging_id"]')?.value || '0', 10);
  const options = row._packagingRows || [];
  return options.find(p => Number(p.id || 0) === id) || null;
}

function updateManualRowPreview(row){
  const p = manualSelectedPackaging(row);
  const palletsRaw = parseFloat(String(row.querySelector('[name="item_pallets_qty"]')?.value || '0').replace(',', '.')) || 0;
  const productId = parseInt(row.querySelector('[name="item_product_id"]')?.value || '0', 10);
  const preview = row.querySelector('[data-role="calc_preview"]');
  const hint = row.querySelector('[data-role="item_hint"]');
  const current = manualRouteStockMap instanceof Map ? (manualRouteStockMap.get(productId) || null) : null;

  if (!p || palletsRaw <= 0) {
    if (preview) preview.value = 'Paquetes / Botellas';
    if (hint) {
      const currentTxt = current ? ` Actual en ruta: <b>${fmt(current.current_qty || 0)}</b> pz.` : '';
      hint.innerHTML = 'Selecciona embalaje y captura tarimas.' + currentTxt;
    }
    return;
  }

  const packagesPerPallet = Number(p.packages_per_pallet || 0);
  const piecesPerPackage = Number(p.pieces_per_package || 0);
  const totalPackages = palletsRaw * packagesPerPallet;
  const totalBottles = totalPackages * piecesPerPackage;
  if (preview) preview.value = `${totalPackages.toLocaleString('es-MX')} paquetes · ${totalBottles.toLocaleString('es-MX')} botellas`;

  if (hint) {
    const base = `Paq. x tarima: <b>${esc(packagesPerPallet)}</b> · Botellas x paquete: <b>${esc(piecesPerPackage)}</b>`;
    if (current) {
      const after = Number(current.current_qty || 0) + totalBottles;
      hint.innerHTML = `${base}<br>Actual en ruta: <b>${fmt(current.current_qty || 0)}</b> pz · Con esta carga: <b>${fmt(after)}</b> pz`;
    } else {
      hint.innerHTML = `${base}<br>Este artículo no trae existencia en la ruta; se agregará como nuevo saldo al confirmar carga.`;
    }
  }
}

async function loadManualPackagingOptions(row){
  const productId = parseInt(row.querySelector('[name="item_product_id"]')?.value || '0', 10);
  const sel = row.querySelector('[name="item_packaging_id"]');
  row._packagingRows = [];
  if (!productId || !sel) {
    if (sel) sel.innerHTML = '<option value="0">Selecciona producto</option>';
    updateManualRowPreview(row);
    return;
  }
  sel.innerHTML = '<option value="0">Cargando embalajes...</option>';
  const j = await apiGet('botella_manual_packaging_options', {product_id: productId});
  const rows = j.ok ? (j.rows || []) : [];
  row._packagingRows = rows;
  if (!rows.length) {
    sel.innerHTML = '<option value="0">Sin embalaje configurado</option>';
  } else {
    sel.innerHTML = rows.map(p => `<option value="${Number(p.id || 0)}">${esc(p.packaging_name || p.packaging_type || 'Embalaje')} · ${esc(p.packages_per_pallet)} paq/tar · ${esc(p.pieces_per_package)} bot/paq${Number(p.legacy_fallback||0)===1 ? ' · principal' : ''}</option>`).join('');
  }
  updateManualRowPreview(row);
}

function updateRouteItemsTotal(){
  const rows = Array.from(document.querySelectorAll('#itemsWrap .item-row'));
  let totalPallets = 0;
  for (const row of rows) {
    totalPallets += parseFloat(String(row.querySelector('[name="item_pallets_qty"]')?.value || '0').replace(',', '.')) || 0;
  }
  if ($('routeLoadPallets')) $('routeLoadPallets').textContent = totalPallets.toLocaleString('es-MX', {maximumFractionDigits:0});
  const alertBox = $('routeItemsAlert');
  if (alertBox) {
    if (totalPallets <= 0) {
      alertBox.classList.remove('d-none');
      alertBox.textContent = 'Agrega al menos un artículo con cantidad de tarimas.';
    } else {
      alertBox.classList.add('d-none');
      alertBox.textContent = '';
    }
  }
  return totalPallets;
}

function createItemRow(item = {}){
  const div = document.createElement('div');
  div.className = 'item-row';
  div.innerHTML = `
    <div class="row g-3 align-items-end">
      <div class="col-12 col-lg-4">
        <label class="form-label">Producto</label>
        <select class="form-select" name="item_product_id">${productOptions()}</select>
      </div>
      <div class="col-12 col-lg-3">
        <label class="form-label">Embalaje</label>
        <select class="form-select" name="item_packaging_id"><option value="0">Selecciona producto</option></select>
      </div>
      <div class="col-6 col-lg-2">
        <label class="form-label">Cantidad de tarimas</label>
        <input class="form-control mono" name="item_pallets_qty" inputmode="decimal" value="${esc(item.pallets_qty || '')}" placeholder="0">
      </div>
      <div class="col-6 col-lg-3">
        <label class="form-label">Cálculo automático</label>
        <input class="form-control mono" data-role="calc_preview" value="Paquetes / Botellas" readonly>
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Tipo tarima</label>
        <select class="form-select" name="item_pallet_type">
          <option value="Chep">Chep</option>
          <option value="Gris">Gris</option>
        </select>
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Especificación tarima</label>
        <select class="form-select" name="item_pallet_specification">
          <option value="retorno">retorno</option>
          <option value="surtido">surtido</option>
        </select>
      </div>
      <div class="col-12 col-md-3">
        <div class="small muted" data-role="item_hint">Selecciona embalaje y captura tarimas.</div>
      </div>
      <div class="col-12 col-md-1">
        <button type="button" class="btn toolbar-btn w-100 btnRemoveItem">
          <i class="bi bi-x-lg"></i>
        </button>
      </div>
    </div>
  `;

  const prodSel = div.querySelector('[name="item_product_id"]');
  if (prodSel) prodSel.value = String(item.product_id || products[0]?.id || '');

  const typeSel = div.querySelector('[name="item_pallet_type"]');
  if (typeSel && item.pallet_type) typeSel.value = String(item.pallet_type);
  const specSel = div.querySelector('[name="item_pallet_specification"]');
  if (specSel && item.pallet_specification) specSel.value = String(item.pallet_specification);

  const removeBtn = div.querySelector('.btnRemoveItem');
  if (removeBtn) removeBtn.addEventListener('click', () => { div.remove(); updateRouteItemsTotal(); });
  if (prodSel) prodSel.addEventListener('change', () => loadManualPackagingOptions(div));
  div.querySelector('[name="item_packaging_id"]')?.addEventListener('change', () => updateManualRowPreview(div));
  div.querySelector('[name="item_pallets_qty"]')?.addEventListener('input', () => { updateManualRowPreview(div); updateRouteItemsTotal(); });

  loadManualPackagingOptions(div).then(() => {
    if (item.packaging_option_id !== undefined) {
      const packSel = div.querySelector('[name="item_packaging_id"]');
      if (packSel) packSel.value = String(item.packaging_option_id || 0);
      updateManualRowPreview(div);
    }
  });

  return div;
}

async function openModal(){
  $('editForm').reset();
  $('itemsWrap').innerHTML = '';
  const firstChannel = channels[0] || null;
  if ($('fChannelHidden')) $('fChannelHidden').value = firstChannel ? String(firstChannel.id || 0) : '0';
  if ($('fDeliveryAddress')) $('fDeliveryAddress').value = '';
  if ($('fCustomerCode')) $('fCustomerCode').value = '';
  if ($('fCustomerBusinessName')) $('fCustomerBusinessName').value = '';
  if ($('fCustomerLocationLabel')) $('fCustomerLocationLabel').value = '';
  if ($('fRouteLabel')) $('fRouteLabel').value = '';
  if ($('routeLoadPallets')) $('routeLoadPallets').textContent = '0';
  if ($('routeItemsAlert')) { $('routeItemsAlert').classList.add('d-none'); $('routeItemsAlert').textContent = ''; }

  $('editForm').querySelector('[name="work_date"]').value = operationalDateForForms();
  const now = new Date();
  const pad = n => String(n).padStart(2, '0');
  const localDateTime = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
  const depart = $('editForm').querySelector('[name="depart_at"]');
  if (depart) depart.value = localDateTime;
  const delivery = $('editForm').querySelector('[name="delivery_date"]');
  if (delivery) delivery.value = operationalDateForForms();

  await loadManualOutRoutes();
  await loadManualOutCustomers();
  if ($('fOrderType')) $('fOrderType').value = 'RUTA';
  updateManualOutOrderType();
  $('itemsWrap').appendChild(createItemRow());
  updateRouteItemsTotal();
  modal.show();
}

function openTransferModal(){
  $('transferForm').reset();
  $('tProduct').innerHTML = productOptions();
  $('tFromWarehouse').innerHTML = warehouseOptions();
  $('tToWarehouse').innerHTML = warehouseOptions();

  $('transferForm').querySelector('[name="work_date"]').value = operationalDateForForms();

  transferModal.show();
}

function collectForm(){
  const fd = new FormData($('editForm'));
  const payload = Object.fromEntries(fd.entries());

  payload.channel_id = parseInt(payload.channel_id || '0', 10);
  payload.order_type = String(payload.order_type || 'RUTA').toUpperCase();
  payload.route_no = parseInt(payload.route_no || '0', 10);
  payload.customer_id = parseInt(payload.customer_id || '0', 10);
  payload.address_id = parseInt(payload.address_id || '0', 10);
  payload.items = [];

  document.querySelectorAll('#itemsWrap .item-row').forEach(row => {
    const productId = parseInt(row.querySelector('[name="item_product_id"]')?.value || '0', 10);
    const packagingId = parseInt(row.querySelector('[name="item_packaging_id"]')?.value || '0', 10);
    const palletsQty = parseFloat(String(row.querySelector('[name="item_pallets_qty"]')?.value || '0').replace(',', '.')) || 0;
    const palletType = row.querySelector('[name="item_pallet_type"]')?.value || '';
    const palletSpecification = row.querySelector('[name="item_pallet_specification"]')?.value || '';
    if (productId > 0 && palletsQty > 0) {
      payload.items.push({
        product_id: productId,
        packaging_option_id: packagingId,
        pallets_qty: palletsQty,
        pallet_type: palletType,
        pallet_specification: palletSpecification
      });
    }
  });

  return payload;
}

function collectTransferForm(){
  const fd = new FormData($('transferForm'));
  const payload = Object.fromEntries(fd.entries());

  payload.product_id = parseInt(payload.product_id || '0', 10);
  payload.from_warehouse_id = parseInt(payload.from_warehouse_id || '0', 10);
  payload.to_warehouse_id = parseInt(payload.to_warehouse_id || '0', 10);
  payload.qty = parseFloat(String(payload.qty || '0').replace(',', '.')) || 0;

  return payload;
}

function buildOsaCreatedSummaryHtml(payload, response){
  const routeLabel = $('fRouteDisplay')?.value || $('fRouteLabel')?.value || 'Ruta de reparto';
  const rows = Array.from(document.querySelectorAll('#itemsWrap .item-row'));
  let totalPallets = 0;
  let totalPackages = 0;
  let totalPieces = 0;

  const itemRows = rows.map(row => {
    const productId = parseInt(row.querySelector('[name="item_product_id"]')?.value || '0', 10);
    const prod = products.find(p => Number(p.id || 0) === productId) || {};
    const packaging = manualSelectedPackaging(row) || {};
    const pallets = parseFloat(String(row.querySelector('[name="item_pallets_qty"]')?.value || '0').replace(',', '.')) || 0;
    if (!productId || pallets <= 0) return '';

    const packagesPerPallet = Number(packaging.packages_per_pallet || 0);
    const piecesPerPackage = Number(packaging.pieces_per_package || 0);
    const packages = pallets * packagesPerPallet;
    const pieces = packages * piecesPerPackage;
    totalPallets += pallets;
    totalPackages += packages;
    totalPieces += pieces;

    return `
      <tr>
        <td>
          <div class="fw-bold">${esc(prod.sku || '')}</div>
          <div class="small muted">${esc(prod.name || '')}</div>
        </td>
        <td>${esc(packaging.packaging_name || packaging.packaging_type || 'Embalaje')}</td>
        <td class="text-end mono">${fmt(pallets)}</td>
        <td class="text-end mono">${fmt(packages)}</td>
        <td class="text-end mono fw-bold">${fmt(pieces)}</td>
      </tr>`;
  }).filter(Boolean).join('');

  return `
    <div class="osa-summary-created">
      <div class="alert alert-success mb-3">
        <div class="fw-bold">OSA creada correctamente</div>
        <div class="small">${esc(response?.message || 'La carga a ruta quedó registrada.')}</div>
      </div>
      <div class="row g-2 mb-3">
        <div class="col-12 col-md-6">
          <div class="soft-box">
            <div class="small muted">Ruta</div>
            <div class="fw-bold">${esc(routeLabel)}</div>
          </div>
        </div>
        <div class="col-12 col-md-6">
          <div class="soft-box">
            <div class="small muted">Fecha operativa</div>
            <div class="fw-bold mono">${esc(payload.work_date || operationalDateForForms())}</div>
          </div>
        </div>
      </div>
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead>
            <tr>
              <th>Artículo</th>
              <th>Embalaje</th>
              <th class="text-end">Tarimas</th>
              <th class="text-end">Paquetes</th>
              <th class="text-end">Botellas</th>
            </tr>
          </thead>
          <tbody>${itemRows || '<tr><td colspan="5" class="text-center muted py-3">Sin artículos capturados.</td></tr>'}</tbody>
          <tfoot>
            <tr>
              <th colspan="2" class="text-end">Total</th>
              <th class="text-end mono">${fmt(totalPallets)}</th>
              <th class="text-end mono">${fmt(totalPackages)}</th>
              <th class="text-end mono">${fmt(totalPieces)}</th>
            </tr>
          </tfoot>
        </table>
      </div>
      <div class="small muted mt-3">
        La OSA ya afectó inventario como carga a ruta. No se abrirá la pantalla adicional de carga; OSV conserva su flujo normal hacia montacargas/camión.
      </div>
    </div>`;
}

async function saveOut(){
  const payload = collectForm();
  const isOsaRoute = String(payload.order_type || 'RUTA').toUpperCase() !== 'CLIENTE';
  const osaSummaryHtml = isOsaRoute ? buildOsaCreatedSummaryHtml(payload, null) : '';

  const j = await apiPost('out_save', payload);
  if (!j.ok) {
    if (j.validation_code === 'PALLET_DECIMAL' || j.validation_code === 'LOGISTICS' || j.validation_code === 'OUT_ERROR') {
      $('botellaPalletAlertText').textContent = j.error || 'La información logística debe revisarse antes de guardar la salida.';
      botellaPalletAlertModal.show();
    } else {
      alert(j.error || 'No se pudo guardar');
    }
    return;
  }

  modal.hide();
  await reloadAll();

  if (isOsaRoute) {
    await appAlert(buildOsaCreatedSummaryHtml(payload, j) || osaSummaryHtml, {
      title: 'Resumen de OSA',
      sub: 'Carga a ruta registrada',
      okText: 'Aceptar'
    });
    return;
  }

  const truckUrl = j.truck_url || (j.order_id ? `carga_camion_botella.php?order_id=${encodeURIComponent(j.order_id)}` : 'carga_camion_botella.php');
  const loadLabel = truckUrl.includes('carga_ruta_botella.php') ? 'Cargar ruta' : 'Cargar camión';
  if (await appConfirm((j.message || 'Orden creada.') + `\n\n¿Deseas abrir ${loadLabel} ahora?`, {title:'Orden creada', okText:`Abrir ${loadLabel}`})) {
    window.location.href = truckUrl;
  }
}

async function saveTransfer(){
  const payload = collectTransferForm();
  const j = await apiPost('transfer_save', payload);
  if (!j.ok) {
    alert(j.error || 'No se pudo guardar la transferencia');
    return;
  }
  transferModal.hide();
  await reloadAll();
  alert('Transferencia guardada correctamente.');
}

async function delMove(id){
  if (!(await appConfirm('¿Deseas eliminar este movimiento?', {title:'Eliminar movimiento', okText:'Eliminar', okClass:'btn btn-danger'}))) return;
  const j = await apiPost('delete', {id});
  if (!j.ok) {
    alert(j.error || 'No se pudo eliminar');
    return;
  }
  await reloadAll();
}

async function openDetail(){
  const j = await apiGet('bars_detail');
  if (!j.ok) {
    alert(j.error || 'No se pudo cargar el detalle');
    return;
  }

  $('barsUsedKpi').textContent = fmtBar(j.bars_used || 0);
  $('detailNote').innerHTML = `<i class="bi bi-info-circle me-2"></i>${esc(j.note || '')}`;

  const bw = $('barsWhBody');
  bw.innerHTML = '';
  for (const r of (j.bars_by_wh || [])) {
    bw.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="fw-semibold" >${esc(r.code)} · ${esc(r.name)}</td>
        <td class="text-end mono fw-bold">${fmtBar(r.qty)}</td>
      </tr>
    `);
  }
  if (!(j.bars_by_wh || []).length) {
    bw.innerHTML = `<tr><td colspan="2" class="text-center muted py-3">Sin barra comercial en CF1/CF2.</td></tr>`;
  }

  const bi = $('barsInternalBody');
  if (bi) {
    bi.innerHTML = '';
    for (const r of (j.bars_internal_wh || [])) {
      bi.insertAdjacentHTML('beforeend', `
        <tr>
          <td>
            <div class="fw-semibold">${esc(r.code)} · ${esc(r.name)}</div>
            <div class="small muted">Inventario físico interno; no suma al disponible comercial.</div>
          </td>
          <td class="text-end mono fw-bold ${num(r.qty) < 0 ? 'text-danger' : ''}">${fmtBar(r.qty)}</td>
        </tr>
      `);
    }
    if (!(j.bars_internal_wh || []).length) {
      bi.innerHTML = `<tr><td colspan="2" class="text-center muted py-3">Sin barra en almacenes internos.</td></tr>`;
    }
  }

  const cf = $('ptBody');
  cf.innerHTML = '';
  for (const r of (j.pt_products || [])) {
    cf.insertAdjacentHTML('beforeend', `
      <tr>
        <td>
          <div class="fw-semibold" >${esc(r.sku)}</div>
          <div class="small" >${esc(r.name)}</div>
        </td>
        <td class="text-end mono fw-bold">${fmt(r.qty)}</td>
        <td class="mono">${esc(r.uom || '')}</td>
      </tr>
    `);
  }
  if (!(j.pt_products || []).length) {
    cf.innerHTML = `<tr><td colspan="3" class="text-center muted py-3">Sin productos.</td></tr>`;
  }

  detailModal.show();
}

async function openSalesOrderDetail(orderId){
  const j = await apiGet('vh_order_detail', {order_id: orderId});
  if (!j.ok) {
    alert(j.error || 'No se pudo cargar el detalle del pedido de VentasHielo.');
    return;
  }

  currentSalesOrder = Object.assign({}, j.order || {}, {source_mode:'VH'});
  const order = j.order || {};
  const items = j.items || [];
  const folio = order.sales_order || order.ov_system || ('PW-' + order.id);

  $('sdOrderId').value = String(order.id || '');
  $('sdChannelId').innerHTML = channelOptions();
  $('sdWorkDate').value = operationalDateForForms();
  $('sdReference').value = folio;
  $('sdCustomer').value = order.cliente || '';
  $('sdDriverName').value = '';
  $('sdVehicleUnit').value = order.vehicle_unit || '';
  $('sdDepartAt').value = '';
  $('sdNotes').value = order.comments || '';

  $('salesDetailHeader').innerHTML = `
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Folio</div>
          <div class="fw-bold mono">${esc(folio)}</div>
        </div>
      </div>
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Cliente</div>
          <div class="fw-bold">${esc(order.cliente || '')}</div>
          <div class="small">${esc(order.cliente_phone || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Fecha solicitada</div>
          <div class="fw-semibold">${esc(order.delivery_date || '')} ${esc(order.delivery_time || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="item-row">
          <div class="small">Dirección entrega</div>
          <div class="fw-semibold">${esc(order.delivery_address || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="item-row">
          <div class="small">Lugar / etiqueta</div>
          <div class="fw-semibold">${esc(order.address_label || '—')}</div>
        </div>
      </div>
      <div class="col-12">
        <div class="item-row">
          <div class="small">Instrucciones de entrega</div>
          <div class="fw-semibold">${esc(order.delivery_instructions || 'Sin instrucciones de entrega')}</div>
        </div>
      </div>
    `;

  $('salesDetailItems').innerHTML = items.map(it => `
    <tr>
      <td class="mono">${esc(it.breport_product_id || '')}</td>
      <td>
        <div class="fw-semibold">${esc(it.product_name || '')}</div>
        <div class="small">VH #${esc(it.product_id || '')}</div>
      </td>
      <td class="text-end mono fw-bold">${fmt(it.quantity)}</td>
      <td class="mono">PZA</td>
    </tr>
  `).join('');

  $('btnDispatchSalesOrder').classList.toggle('d-none', !CAN_WRITE);
  if ($('btnUploadInvoice')) $('btnUploadInvoice').classList.add('d-none');
  if ($('btnSalesOrderPdf')) {
    $('btnSalesOrderPdf').classList.add('d-none');
    $('btnSalesOrderPdf').href = '#';
  }
  if ($('btnSalesOrderTicketPdf')) {
    $('btnSalesOrderTicketPdf').classList.add('d-none');
    $('btnSalesOrderTicketPdf').href = '#';
  }
  salesDetailModal.show();
}




function renderOrderFilesList(title, rows, emptyText, icon){
  if (!Array.isArray(rows) || !rows.length) {
    return `<div class="item-row"><div class="small">${esc(title)}</div><div class="muted small">${esc(emptyText)}</div></div>`;
  }
  const links = rows.map(a => {
    const href = String(a.file_path || '').trim();
    const label = String(a.original_name || a.stored_name || href || 'Archivo').trim();
    const extra = a.purchase_order ? ` · OC ${esc(a.purchase_order)}` : (a.invoice_number ? ` · Factura ${esc(a.invoice_number)}` : '');
    return href
      ? `<a class="btn btn-sm btn-blue-soft me-2 mb-2" href="${esc(href)}" target="_blank" rel="noopener"><i class="bi ${icon} me-1"></i>${esc(label)}${extra}</a>`
      : `<span class="badge bg-light text-dark me-2 mb-2">${esc(label)}${extra}</span>`;
  }).join('');
  return `<div class="item-row"><div class="small mb-2">${esc(title)}</div><div>${links}</div></div>`;
}

function openInvoiceUploadModal(){
  if (!currentSalesOrder || !currentSalesOrder.id) {
    alert('Selecciona primero una orden.');
    return;
  }
  if (!currentSalesOrder.outbound_id) {
    alert('Primero genera la salida de almacén antes de cargar factura.');
    return;
  }
  $('invoiceOrderId').value = String(currentSalesOrder.id || '0');
  $('invoiceNumber').value = '';
  $('invoiceFile').value = '';
  $('invoiceUploadMsg').innerHTML = '';
  invoiceUploadModal.show();
}

async function submitInvoiceUpload(e){
  e.preventDefault();
  const form = $('invoiceUploadForm');
  const fd = new FormData(form);
  const btn = form.querySelector('button[type="submit"]');
  const original = btn.innerHTML;
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
  $('invoiceUploadMsg').innerHTML = '';
  try {
    const j = await apiUpload('botella_invoice_upload', fd);
    if (!j.ok) {
      $('invoiceUploadMsg').innerHTML = `<span class="text-danger fw-semibold">${esc(j.error || 'No se pudo cargar factura.')}</span>`;
      return;
    }
    $('invoiceUploadMsg').innerHTML = `<span class="text-success fw-semibold">${esc(j.message || 'Factura cargada correctamente.')}</span>`;
    invoiceUploadModal.hide();
    await loadDispatchMailbox();
    if (currentSalesOrder?.id) await openBreportSalesOrderDetail(currentSalesOrder.id);
  } finally {
    btn.disabled = false;
    btn.innerHTML = original;
  }
}

async function openBreportSalesOrderDetail(orderId){
  const j = await apiGet('sales_order_detail', {order_id: orderId});
  if (!j.ok) {
    alert(j.error || 'No se pudo cargar el detalle de la orden creada desde ventas.');
    return;
  }

  currentSalesOrder = Object.assign({}, j.order || {}, {source_mode:'BREPORT'});
  const order = j.order || {};
  const routeOrder = isRouteLogisticsOrder(order);
  const items = j.items || [];
  const attachments = j.attachments || [];
  const invoices = j.invoices || [];
  const fork = j.forklift_order || null;
  const truckPlan = j.truck_plan || null;
  const forkliftStatus = String(fork?.status || '').toUpperCase();
  const forkliftReady = forkliftStatus === 'COMPLETA';
  const folio = order.sales_order_ref || ('PED-' + order.id);

  $('sdOrderId').value = String(order.id || '');
  $('sdChannelId').innerHTML = channelOptions();
  $('sdWorkDate').value = operationalDateForForms();
  $('sdReference').value = folio;
  $('sdCustomer').value = order.business_name || '';
  $('sdDriverName').value = '';
  $('sdVehicleUnit').value = fork?.vehicle_unit || '';
  $('sdVehicleUnit').readOnly = true;
  $('sdDepartAt').value = '';
  $('sdNotes').value = order.comments || '';

  $('salesDetailHeader').innerHTML = `
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Folio</div>
          <div class="fw-bold mono">${esc(folio)}</div>
        </div>
      </div>
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Cliente / destino</div>
          <div class="fw-bold">${esc(order.business_name || '')}</div>
          <div class="small">${esc(order.phone || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Fecha solicitada</div>
          <div class="fw-semibold">${esc(order.requested_delivery_date || '')} ${esc(order.requested_delivery_time || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="item-row">
          <div class="small">Dirección / referencia</div>
          <div class="fw-semibold">${esc(order.delivery_address || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-3">
        <div class="item-row">
          <div class="small">Ruta</div>
          <div class="fw-semibold mono">${esc(order.route_no ? ('Ruta ' + order.route_no) : 'Sin ruta')}</div>
        </div>
      </div>
      <div class="col-12 col-md-3">
        <div class="item-row">
          <div class="small">Estatus</div>
          <div class="fw-semibold">${esc(order.status || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="item-row">
          <div class="small">Origen</div>
          <div class="fw-semibold">${routeOrder ? 'Orden logística creada desde Almacén por ruta' : 'Orden creada desde Ventas'}</div>
        </div>
      </div>
      <div class="col-12">
        <div class="item-row">
          <div class="small">Notas registradas en Ventas</div>
          <div class="fw-semibold">${esc(order.comments || 'Sin notas registradas')}</div>
        </div>
      </div>
      <div class="col-12 col-lg-6">
        ${renderOrderFilesList('OC adjunta del cliente', attachments, 'No hay OC adjunta registrada para esta orden.', 'bi-paperclip')}
      </div>
      <div class="col-12 col-lg-6">
        ${renderOrderFilesList('Factura cargada', invoices, 'Aún no se ha cargado factura.', 'bi-receipt-cutoff')}
      </div>
      <div class="col-12">
        <div class="item-row">
          <div class="small">Estado de carga montacargas</div>
          <div class="fw-semibold ${forkliftReady ? 'text-success' : 'text-danger'}">
            ${fork ? `Orden montacargas #${esc(fork.id)} · ${esc(fork.status || '')} · Transporte ${esc(fork.vehicle_unit || '')}` : 'Sin orden de montacargas generada'}
          </div>
          <div class="small muted">
            ${forkliftReady ? 'La carga está confirmada; Almacén puede generar la salida a ruta.' : 'No se permite generar salida hasta que Montacargas confirme carga completa.'}
          </div>
        </div>
      </div>
    `;

  $('salesDetailItems').innerHTML = items.map(it => `
    <tr>
      <td class="mono">${esc(it.sku || '')}</td>
      <td>
        <div class="fw-semibold">${esc(it.product_name || '')}</div>
        <div class="small">Producto BReport #${esc(it.product_id || '')}</div>
      </td>
      <td>
        <div class="fw-semibold">${esc(it.packaging_name || it.packaging_type || 'Sin embalaje')}</div>
        <div class="small muted">${esc(it.pallet_type || '')} ${esc(it.pallet_specification || '')}</div>
      </td>
      <td class="text-end mono fw-bold">${fmt(it.packages_qty || 0)}</td>
      <td class="text-end mono fw-bold">${fmt(it.pallets_qty || 0)}</td>
      <td class="text-end mono fw-bold">${fmt(it.qty || 0)}</td>
    </tr>
  `).join('');

  if ($('btnSalesTruckPlan')) {
    $('btnSalesTruckPlan').innerHTML = routeOrder
      ? '<i class="bi bi-truck-front me-1"></i>Carga ruta / Montacargas'
      : '<i class="bi bi-truck-front me-1"></i>Carga camión / Montacargas';
    $('btnSalesTruckPlan').dataset.routeOrder = routeOrder ? '1' : '0';
    $('btnSalesTruckPlan').dataset.routeNo = String(order.route_no || '');
    $('btnSalesTruckPlan').dataset.workDate = String(order.requested_delivery_date || '');
    const canOpenTruckPlan = CAN_WRITE && String(order.status || '').toUpperCase() === 'AUTORIZADA' && !order.outbound_id && !forkliftReady;
    $('btnSalesTruckPlan').classList.toggle('d-none', !canOpenTruckPlan);
  }

  const canDispatchOrder = CAN_WRITE && String(order.status || '').toUpperCase() === 'AUTORIZADA' && !order.outbound_id && forkliftReady;
  $('btnDispatchSalesOrder').classList.toggle('d-none', !canDispatchOrder);
  if ($('btnUploadInvoice')) {
    const canUploadInvoice = CAN_WRITE && !!order.outbound_id && String(order.status || '').toUpperCase() !== 'ENTREGADA';
    $('btnUploadInvoice').classList.toggle('d-none', !canUploadInvoice);
  }

  if ($('btnSalesOrderPdf')) {
    const hasPdf = !!order.outbound_id;
    const pdfUrl = `./orden_salida_botella_pdf.php?formato=carta&order_id=${encodeURIComponent(order.id || '')}&outbound_id=${encodeURIComponent(order.outbound_id || '')}&folio=${encodeURIComponent(folio)}`;
    const ticketUrl = `./orden_salida_botella_pdf.php?formato=ticket&order_id=${encodeURIComponent(order.id || '')}&outbound_id=${encodeURIComponent(order.outbound_id || '')}&folio=${encodeURIComponent(folio)}`;
    $('btnSalesOrderPdf').classList.toggle('d-none', !hasPdf);
    $('btnSalesOrderPdf').href = hasPdf ? pdfUrl : '#';
    if ($('btnSalesOrderTicketPdf')) {
      $('btnSalesOrderTicketPdf').classList.toggle('d-none', !hasPdf);
      $('btnSalesOrderTicketPdf').href = hasPdf ? ticketUrl : '#';
    }
  }

  salesDetailModal.show();
}

function buildPendingHtml(items){
  if (!Array.isArray(items) || !items.length) {
    return '<div class="text-success fw-semibold"><i class="bi bi-check2-circle me-1"></i>Todos los artículos relacionados disponibles fueron procesados.</div>';
  }

  const rows = items.map(it => {
    const sku = it.sku ? `<div class="small mono text-muted">${esc(it.sku)}</div>` : '';
    const name = esc(it.product_name || ('Producto ' + (it.product_id || it.vh_product_id || '')));
    const req = fmt(it.requested_qty || 0);
    const disp = fmt(it.available_qty || 0);
    const surt = fmt(it.dispatched_qty || 0);
    const falt = fmt(it.missing_qty || 0);
    const reason = esc(it.reason || 'Artículo omitido o surtido parcial.');
    return `
      <tr>
        <td>${name}${sku}<div class="small text-muted">${reason}</div></td>
        <td class="text-end mono">${req}</td>
        <td class="text-end mono">${disp}</td>
        <td class="text-end mono fw-semibold">${surt}</td>
        <td class="text-end mono text-danger fw-semibold">${falt}</td>
      </tr>
    `;
  }).join('');

  return `
    <div class="alert alert-warning mb-2">
      <i class="bi bi-info-circle me-2"></i>
      Algunos artículos no se descontaron o se surtieron parcialmente.
    </div>
    <div class="table-responsive">
      <table class="table table-sm align-middle mb-0">
        <thead>
          <tr>
            <th>Artículo</th>
            <th class="text-end">Solicitado</th>
            <th class="text-end">Disponible</th>
            <th class="text-end">A surtir</th>
            <th class="text-end">Faltante</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>
    </div>
  `;
}

function buildDispatchableHtml(items){
  if (!Array.isArray(items) || !items.length) {
    return '<div class="alert alert-danger mb-0">No hay artículos con inventario disponible para generar salida.</div>';
  }
  const rows = items.map(it => `
    <tr>
      <td>
        <div class="fw-semibold">${esc(it.product_name || '')}</div>
        <div class="small mono text-muted">${esc(it.sku || '')}</div>
      </td>
      <td class="text-end mono">${fmt(it.requested_qty || 0)}</td>
      <td class="text-end mono">${fmt(it.available_qty || 0)}</td>
      <td class="text-end mono fw-bold text-success">${fmt(it.dispatched_qty || 0)}</td>
    </tr>
  `).join('');
  return `
    <div class="table-responsive">
      <table class="table table-sm align-middle mb-0">
        <thead><tr><th>Artículo</th><th class="text-end">Solicitado</th><th class="text-end">Disponible</th><th class="text-end">Se surtirá</th></tr></thead>
        <tbody>${rows}</tbody>
      </table>
    </div>
  `;
}

function buildMovementHtml(items){
  if (!Array.isArray(items) || !items.length) return '';
  const rows = items.map(it => `
    <tr>
      <td>
        <div class="fw-semibold">${esc(it.product_name || '')}</div>
        <div class="small mono text-muted">${esc(it.sku || '')}</div>
      </td>
      <td class="mono">${esc(it.warehouse_code || '')}</td>
      <td class="mono">${esc(it.move_type || 'OUT')}</td>
      <td class="text-end mono fw-bold">${fmt(it.qty || 0)}</td>
      <td class="mono">${esc(it.ref_type || '')}</td>
    </tr>
  `).join('');
  return `
    <div class="mt-3">
      <div class="fw-semibold mb-2">Movimientos generados en inventario</div>
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead><tr><th>Artículo</th><th>Almacén</th><th>Mov.</th><th class="text-end">Cantidad</th><th>Tipo</th></tr></thead>
          <tbody>${rows}</tbody>
        </table>
      </div>
    </div>
  `;
}

function showVhPartialConfirm(data){
  let el = document.getElementById('vhPartialConfirmModal');
  if (!el) {
    document.body.insertAdjacentHTML('beforeend', `
      <div class="modal fade" id="vhPartialConfirmModal" tabindex="-1">
        <div class="modal-dialog modal-dialog-centered modal-xl">
          <div class="modal-content rounded-4">
            <div class="modal-header">
              <div>
                <div class="modal-title fw-bold">Confirmar salida parcial VentasHielo</div>
                <div class="small text-muted">Solo se descontarán artículos con existencia disponible en BReport.</div>
              </div>
              <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
              <div class="alert alert-warning">
                <i class="bi bi-exclamation-triangle me-2"></i>
                El pedido tiene artículos sin inventario suficiente o sin relación con BReport. Revisa el detalle antes de continuar.
              </div>
              <div class="row g-3">
                <div class="col-12 col-xl-6">
                  <div class="fw-semibold mb-2">Artículos que sí se surtirán</div>
                  <div id="vhPartialOk"></div>
                </div>
                <div class="col-12 col-xl-6">
                  <div class="fw-semibold mb-2">Artículos sin existencia / pendientes</div>
                  <div id="vhPartialPending"></div>
                </div>
              </div>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
              <button type="button" class="btn btn-primary" id="btnVhPartialConfirmYes">Generar salida parcial</button>
            </div>
          </div>
        </div>
      </div>
    `);
    el = document.getElementById('vhPartialConfirmModal');
  }

  document.getElementById('vhPartialOk').innerHTML = buildDispatchableHtml(data.dispatchable_items || []);
  document.getElementById('vhPartialPending').innerHTML = buildPendingHtml(data.pending_items || []);

  const modalInstance = bootstrap.Modal.getOrCreateInstance(el);
  return new Promise(resolve => {
    const btn = document.getElementById('btnVhPartialConfirmYes');
    const yesHandler = () => {
      btn.removeEventListener('click', yesHandler);
      el.removeEventListener('hidden.bs.modal', noHandler);
      modalInstance.hide();
      resolve(true);
    };
    const noHandler = () => {
      btn.removeEventListener('click', yesHandler);
      el.removeEventListener('hidden.bs.modal', noHandler);
      resolve(false);
    };
    btn.addEventListener('click', yesHandler, {once:true});
    el.addEventListener('hidden.bs.modal', noHandler, {once:true});
    modalInstance.show();
  });
}

function confirmDispatch(payload){
  $('confirmDispatchSummary').innerHTML = `
    <div class="item-row mb-2">
      <div class="small">Referencia</div>
      <div class="fw-semibold mono">${esc(payload.reference || '')}</div>
    </div>
    <div class="item-row mb-2">
      <div class="small">Cliente / destino</div>
      <div class="fw-semibold">${esc(payload.customer_name || '')}</div>
    </div>
    <div class="item-row">
      <div class="small">Número de transporte</div>
      <div class="fw-semibold">${esc(payload.vehicle_unit || '-')}</div>
    </div>
  `;

  return new Promise(resolve => {
    const btn = $('btnConfirmDispatchYes');
    const handler = () => {
      btn.removeEventListener('click', handler);
      confirmDispatchModal.hide();
      resolve(true);
    };

    btn.addEventListener('click', handler, {once:true});
    const el = document.getElementById('confirmDispatchModal');
    const cancelHandler = () => {
      btn.removeEventListener('click', handler);
      el.removeEventListener('hidden.bs.modal', cancelHandler);
      resolve(false);
    };
    el.addEventListener('hidden.bs.modal', cancelHandler, {once:true});
    confirmDispatchModal.show();
  });
}



async function openBotellaTruckPlan(orderId){
  const j = await apiGet('botella_load_plan_detail', {order_id: orderId});
  if (!j.ok) {
    alert(j.error || 'No se pudo consultar el plan de carga.');
    return;
  }
  const plan = j.plan || {};
  const fork = j.forklift_order || null;
  $('flOrderId').value = String(orderId || '');
  $('flVehicleUnit').value = fork?.vehicle_unit || plan.vehicle_label || '';
  $('flDriverName').value = fork?.driver_name || '';
  $('flNotes').value = '';
  $('btnBotellaTruckPrint').href = j.print_url || '#';
  $('btnBotellaTruckPrint').classList.toggle('d-none', !j.print_url);
  const statusText = fork
    ? `Orden montacargas #${esc(fork.id)} · ${esc(fork.status)} · Transporte ${esc(fork.vehicle_unit || '')}`
    : 'Aún no se ha enviado orden a montacargas.';
  $('botellaTruckPlanStatus').innerHTML = `<div class="alert ${fork ? (String(fork.status).toUpperCase()==='COMPLETA'?'alert-success':'alert-info') : 'alert-warning'} mb-0"><b>${esc(plan.vehicle_label || 'Unidad')}</b> · ${esc(plan.capacity_pallets || '')} tarimas programadas<br>${statusText}</div>`;
  const rows = j.summary || [];
  $('botellaTruckPlanBody').innerHTML = rows.length ? rows.map(r => `
    <tr>
      <td class="mono fw-bold">${esc(r.purchase_order || '')}</td>
      <td>${esc(r.customer_label || '')}</td>
      <td><div class="small">${esc(r.delivery_address || '')}</div></td>
      <td>${esc(r.product_label || '')}</td>
      <td class="text-end mono fw-bold">${fmt(r.pallets_qty || 0)}</td>
      <td class="text-end mono">${fmt(r.packages_qty || 0)}</td>
      <td class="text-end mono">${fmt(r.pieces_qty || 0)}</td>
      <td class="small mono">${esc((r.slots || []).join(', '))}</td>
    </tr>`).join('') : '<tr><td colspan="8" class="text-center muted py-3">Sin tarimas en el plan.</td></tr>';
  $('btnCreateForkliftOrder').disabled = fork && !['CANCELADA','COMPLETA'].includes(String(fork.status || '').toUpperCase());
  botellaTruckPlanModal.show();
}

async function createForkliftOrder(){
  const payload = {
    order_id: parseInt($('flOrderId').value || '0', 10),
    vehicle_unit: $('flVehicleUnit').value.trim(),
    driver_name: '',
    notes: $('flNotes').value.trim()
  };
  if (payload.order_id <= 0) { alert('Orden inválida.'); return; }
  if (!payload.vehicle_unit) { alert('Indica número de unidad a cargar.'); return; }
  const btn = $('btnCreateForkliftOrder');
  const html = btn.innerHTML;
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Enviando...';
  try {
    const j = await apiPost('botella_create_forklift_order', payload);
    if (!j.ok) { alert(j.error || 'No se pudo crear orden a montacargas.'); return; }
    alert('Orden enviada a montacargas #' + (j.forklift_order_id || ''));
    botellaTruckPlanModal.hide();
    await reloadAll();
  } finally {
    btn.disabled = false;
    btn.innerHTML = html;
  }
}

async function dispatchSalesOrder(){
  const payload = {
    order_id: parseInt($('sdOrderId').value || '0', 10),
    work_date: $('sdWorkDate').value,
    channel_id: parseInt($('sdChannelId').value || '0', 10),
    reference: $('sdReference').value.trim(),
    customer_name: $('sdCustomer').value.trim(),
    driver_name: $('sdDriverName').value.trim(),
    vehicle_unit: $('sdVehicleUnit').value.trim(),
    depart_at: $('sdDepartAt').value.trim(),
    notes: $('sdNotes').value.trim()
  };

  if (payload.order_id <= 0) {
    alert('Pedido inválido.');
    return;
  }
  if (payload.channel_id <= 0) {
    alert('Selecciona un canal.');
    return;
  }
  if (currentSalesOrder && currentSalesOrder.source_mode === 'BREPORT' && !payload.vehicle_unit) {
    alert('No se encontró el número de transporte confirmado por Montacargas. Abre Carga camión / Montacargas y valida la orden de carga.');
    return;
  }

  const confirmed = await confirmDispatch(payload);
  if (!confirmed) return;

  const btn = $('btnDispatchSalesOrder');
  const originalHtml = btn.innerHTML;
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generando salida...';

  try {
    const dispatchAction = currentSalesOrder && currentSalesOrder.source_mode === 'BREPORT' ? 'sales_dispatch' : 'vh_dispatch';
    let j = await apiPost(dispatchAction, payload);

    if (j.ok && j.requires_confirmation && dispatchAction === 'vh_dispatch') {
      const allowPartial = await showVhPartialConfirm(j);
      if (!allowPartial) return;
      payload.confirm_partial = true;
      j = await apiPost(dispatchAction, payload);
    }

    if (!j.ok) {
      if (j.validation_code === 'PALLET_DECIMAL' || j.validation_code === 'LOGISTICS' || j.validation_code === 'FORKLIFT') {
        $('botellaPalletAlertText').textContent = j.error || 'La información logística de la orden debe revisarse antes de surtir.';
        botellaPalletAlertModal.show();
        return;
      }
      const detail = j.pending_items
        ? '\n\n' + (j.pending_items || []).map(it => `${it.product_name || it.sku || 'Artículo'}: solicitado ${fmt(it.requested_qty || 0)}, disponible ${fmt(it.available_qty || 0)}, faltante ${fmt(it.missing_qty || 0)}`).join('\n')
        : '';
      alert((j.error || 'No se pudo generar la salida.') + detail);
      return;
    }

    salesDetailModal.hide();
    await reloadAll();

    const pdfUrl = j.pdf_url || `./orden_salida_botella_pdf.php?formato=carta&order_id=${encodeURIComponent(j.sales_order_id || '')}&outbound_id=${encodeURIComponent(j.outbound_id || '')}&folio=${encodeURIComponent(j.reference || payload.reference || '')}`;
    const ticketUrl = `./orden_salida_botella_pdf.php?formato=ticket&order_id=${encodeURIComponent(j.sales_order_id || '')}&outbound_id=${encodeURIComponent(j.outbound_id || '')}&folio=${encodeURIComponent(j.reference || payload.reference || '')}`;

    $('btnDownloadDispatchPdf').href = pdfUrl;
    if ($('btnDownloadDispatchTicketPdf')) $('btnDownloadDispatchTicketPdf').href = ticketUrl;
    $('dispatchResultSummary').innerHTML = `
      <div class="item-row mb-2">
        <div class="small">Referencia</div>
        <div class="fw-semibold mono">${esc(j.reference || payload.reference || '')}</div>
      </div>
      <div class="item-row mb-3">
        <div class="small">Salida generada</div>
        <div class="fw-semibold">Outbound #${esc(j.outbound_id || '')}</div>
      </div>
      ${buildPendingHtml(j.pending_items || [])}
      ${buildMovementHtml(j.movement_rows || [])}
    `;
    dispatchResultModal.show();
  } finally {
    btn.disabled = false;
    btn.innerHTML = originalHtml;
  }
}


document.addEventListener('DOMContentLoaded', async () => {
  modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('editModal'), {backdrop:'static', keyboard:false});
  detailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('detailModal'));
  transferModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('transferModal'));
  returnModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('returnModal'));
  returnConfirmModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('returnConfirmModal'));
  returnResultModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('returnResultModal'));
  if ($('manualStockInModal')) manualStockInModal = bootstrap.Modal.getOrCreateInstance($('manualStockInModal'));
  if ($('manualStockInConfirmModal')) manualStockInConfirmModal = bootstrap.Modal.getOrCreateInstance($('manualStockInConfirmModal'));
  if ($('manualStockInResultModal')) manualStockInResultModal = bootstrap.Modal.getOrCreateInstance($('manualStockInResultModal'));
  salesDetailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('salesDetailModal'));
  routeOrdersModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('routeOrdersModal'));
  confirmDispatchModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('confirmDispatchModal'));
  dispatchResultModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('dispatchResultModal'));
  if ($('invoiceUploadModal')) invoiceUploadModal = bootstrap.Modal.getOrCreateInstance($('invoiceUploadModal'));
  botellaPalletAlertModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('botellaPalletAlertModal'));
  botellaTruckPlanModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('botellaTruckPlanModal'));
  locReqModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('locReqModal'));
  locReqDetailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('locReqDetailModal'));
  locReqConfirmModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('locReqConfirmModal'));
  locReqRejectModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('locReqRejectModal'));
  movementHistoryModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('movementHistoryModal'));
  if ($('inventoryQuickModal')) inventoryQuickModal = bootstrap.Modal.getOrCreateInstance($('inventoryQuickModal'));
  mermaModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('mermaModal'));

  await loadSystemClock();
  setInterval(loadSystemClock, 30000);
  await loadScopeSelector();
  if ($('btnScopeChange')) $('btnScopeChange').addEventListener('click', changeScopeLocation);
  if ($('scopeLocationSel')) $('scopeLocationSel').addEventListener('change', changeScopeLocation);

  if ($('salesRouteDate')) $('salesRouteDate').value = operationalDateForForms();
  initHistoryDates();

  await loadCatalogs();
  await reloadAll();
  initPendingDispatchWatch();

  if ($('btnReload')) $('btnReload').addEventListener('click', reloadAll);
  if ($('btnAdd')) $('btnAdd').addEventListener('click', () => { if (CAN_WRITE) openModal(); });
  if ($('btnAddQuick')) $('btnAddQuick').addEventListener('click', () => { if (CAN_WRITE) openModal(); });
  if ($('btnInventoryQuick')) $('btnInventoryQuick').addEventListener('click', openInventoryQuickModal);
  if ($('btnInventoryModalRefresh')) $('btnInventoryModalRefresh').addEventListener('click', refreshInventoryQuickModal);
  if ($('btnInventoryModalExport')) $('btnInventoryModalExport').addEventListener('click', exportInventoryModalHistory);
  if ($('btnReturn')) $('btnReturn').addEventListener('click', () => { if (CAN_WRITE) openReturnModal(); });
  if ($('btnReturnSection')) $('btnReturnSection').addEventListener('click', () => { if (CAN_WRITE) openReturnModal(); });
  if ($('btnManualStockIn')) $('btnManualStockIn').addEventListener('click', () => { if (CAN_ADD_EXISTENCE) openManualStockInModal(); });
  if ($('btnManualStockInQuick')) $('btnManualStockInQuick').addEventListener('click', () => { if (CAN_ADD_EXISTENCE) openManualStockInModal(); });
  if ($('btnMerma')) $('btnMerma').addEventListener('click', () => { if (CAN_WRITE) openMermaModal(); });
  if ($('btnMermaQuick')) $('btnMermaQuick').addEventListener('click', () => { if (CAN_WRITE) openMermaModal(); });
  if ($('btnTransfer')) $('btnTransfer').addEventListener('click', () => { if (CAN_WRITE) openTransferModal(); });
  if ($('btnTransferQuick')) $('btnTransferQuick').addEventListener('click', () => { if (CAN_WRITE) openTransferModal(); });
  if ($('btnLocReqNew')) $('btnLocReqNew').addEventListener('click', () => { if (CAN_WRITE) openLocReqModal(); });
  if ($('btnLocReqReload')) $('btnLocReqReload').addEventListener('click', () => loadLocRequests(locReqMode));
  if ($('btnLocReqAddItem')) $('btnLocReqAddItem').addEventListener('click', () => $('locReqItemsWrap').appendChild(createLocReqItemRow()));
  if ($('locReqForm')) $('locReqForm').addEventListener('submit', (e) => { e.preventDefault(); saveLocReq(); });
  if ($('locReqRejectForm')) $('locReqRejectForm').addEventListener('submit', (e) => { e.preventDefault(); submitLocReqReject(); });
  if ($('btnAddItem')) $('btnAddItem').addEventListener('click', () => { $('itemsWrap').appendChild(createItemRow()); updateRouteItemsTotal(); });
  if ($('btnAddManualOutItem')) $('btnAddManualOutItem').addEventListener('click', () => { $('itemsWrap').appendChild(createItemRow()); updateRouteItemsTotal(); });
  if ($('fOrderType')) $('fOrderType').addEventListener('change', updateManualOutOrderType);
  if ($('fCustomer') && $('fCustomer').tagName === 'SELECT') $('fCustomer').addEventListener('change', applyManualOutCustomer);
  if ($('fAddressId') && $('fAddressId').tagName === 'SELECT') $('fAddressId').addEventListener('change', applyManualOutAddress);
  if ($('fRouteNo')) $('fRouteNo').addEventListener('change', applyManualOutRoute);
  const manualWorkDate = $('editForm')?.querySelector('[name="work_date"]');
  if (manualWorkDate) manualWorkDate.addEventListener('change', () => loadSelectedRouteStock());
  $('btnDispatchSalesOrder').addEventListener('click', dispatchSalesOrder);
  if ($('btnUploadInvoice')) $('btnUploadInvoice').addEventListener('click', openInvoiceUploadModal);
  if ($('invoiceUploadForm')) $('invoiceUploadForm').addEventListener('submit', submitInvoiceUpload);
  if ($('btnCreateForkliftOrder')) $('btnCreateForkliftOrder').addEventListener('click', createForkliftOrder);
  if ($('btnSalesTruckPlan')) $('btnSalesTruckPlan').addEventListener('click', () => {
    const btn = $('btnSalesTruckPlan');
    if (btn.dataset.routeOrder === '1') {
      const params = new URLSearchParams();
      if (btn.dataset.routeNo) params.set('route_no', btn.dataset.routeNo);
      if (btn.dataset.workDate) params.set('work_date', btn.dataset.workDate);
      window.open('carga_ruta_botella.php' + (params.toString() ? '?' + params.toString() : ''), '_blank');
      return;
    }
    openBotellaTruckPlan(parseInt($('sdOrderId').value || '0', 10));
  });

  $('editForm').addEventListener('submit', (e) => {
    e.preventDefault();
    saveOut();
  });

  $('transferForm').addEventListener('submit', (e) => {
    e.preventDefault();
    saveTransfer();
  });

  if ($('returnForm')) $('returnForm').addEventListener('submit', (e) => {
    e.preventDefault();
    saveWarehouseReturn();
  });
  if ($('btnConfirmWarehouseReturn')) $('btnConfirmWarehouseReturn').addEventListener('click', confirmWarehouseReturn);
  if ($('returnConfirmModal')) $('returnConfirmModal').addEventListener('hidden.bs.modal', () => {
    if (pendingWarehouseReturnPayload) returnModal.show();
  });
  if ($('rProduct')) $('rProduct').addEventListener('change', loadReturnWarehouses);
  if ($('rWarehouse')) $('rWarehouse').addEventListener('change', refreshReturnWarehouseStockHint);

  if ($('manualStockInForm')) $('manualStockInForm').addEventListener('submit', (e) => {
    e.preventDefault();
    previewManualStockIn();
  });
  if ($('msiProduct')) $('msiProduct').addEventListener('change', async () => {
    await Promise.all([loadManualStockInWarehouses(), loadManualStockInPackaging()]);
    updateManualStockInCalc();
  });
  if ($('msiPackaging')) $('msiPackaging').addEventListener('change', updateManualStockInCalc);
  ['msiRackQty','msiLoosePackages','msiLoosePieces'].forEach(id => {
    if ($(id)) $(id).addEventListener('input', updateManualStockInCalc);
  });
  if ($('btnConfirmManualStockIn')) $('btnConfirmManualStockIn').addEventListener('click', confirmManualStockIn);
  if ($('manualStockInConfirmModal')) $('manualStockInConfirmModal').addEventListener('hidden.bs.modal', () => {
    if (pendingManualStockInPayload && manualStockInModal) manualStockInModal.show();
  });

  $('mermaForm').addEventListener('submit', (e) => {
    e.preventDefault();
    saveWarehouseMerma();
  });

  $('mProduct').addEventListener('change', refreshWarehouseStockHint);
  $('mWarehouse').addEventListener('change', refreshWarehouseStockHint);


  if ($('dispatchMailboxDate')) $('dispatchMailboxDate').value = '';
  document.querySelectorAll('[data-dispatch-folder]').forEach(btn => {
    btn.addEventListener('click', () => {
      dispatchMailboxFolder = btn.dataset.dispatchFolder || 'sales';
      document.querySelectorAll('[data-dispatch-folder]').forEach(x => x.classList.toggle('active', x === btn));
      renderDispatchMailboxList();
    });
  });
  if ($('btnDispatchToday')) $('btnDispatchToday').addEventListener('click', async () => {
    dispatchMailboxAllDates = false;
    $('dispatchMailboxDate').value = operationalDateForForms();
    await loadDispatchMailbox();
  });
  if ($('btnDispatchAll')) {
    $('btnDispatchAll').classList.add('active');
    $('btnDispatchAll').addEventListener('click', async () => {
      dispatchMailboxAllDates = true;
      $('dispatchMailboxDate').value = '';
      await loadDispatchMailbox();
    });
  }
  if ($('dispatchMailboxDate')) $('dispatchMailboxDate').addEventListener('change', async () => {
    dispatchMailboxAllDates = false;
    await loadDispatchMailbox();
  });
  if ($('btnDispatchReload')) $('btnDispatchReload').addEventListener('click', loadDispatchMailbox);
  if ($('dispatchMailboxItems')) $('dispatchMailboxItems').addEventListener('click', (e) => {
    const item = e.target.closest('[data-dispatch-index]');
    if (item) selectDispatchMailboxRow(parseInt(item.dataset.dispatchIndex || '0', 10));
  });
  if ($('dispatchMailboxDetail')) $('dispatchMailboxDetail').addEventListener('click', (e) => {
    const truckBtn = e.target.closest('[data-botella-truck-plan]');
    if (truckBtn) {
      openBotellaTruckPlan(parseInt(truckBtn.dataset.botellaTruckPlan || '0', 10));
      return;
    }
    const vh = e.target.closest('[data-dispatch-open-vh]');
    if (vh) openSalesOrderDetail(parseInt(vh.dataset.dispatchOpenVh || '0', 10));
    const sales = e.target.closest('[data-dispatch-open-sales]');
    if (sales) openBreportSalesOrderDetail(parseInt(sales.dataset.dispatchOpenSales || '0', 10));
  });

  document.querySelectorAll('.operation-nav-btn[data-ops-section]').forEach(btn => {
    btn.addEventListener('click', () => showModuleSection(btn.dataset.opsSection));
  });

  if ($('btnHistoryRefresh')) $('btnHistoryRefresh').addEventListener('click', async () => {
    mermaPage = 1;
    await loadInventoryForSelectedDates();
    await loadMovementSummary();
    renderProductCards();
    await loadWarehouseMermas(1);
  });
  if ($('btnExportMovements')) $('btnExportMovements').addEventListener('click', () => {
    const dateFrom = $('histDateFrom')?.value || getMonthRange().from;
    const dateTo = $('histDateTo')?.value || getMonthRange().to;
    window.location.href = `?action=moves_export_csv&date_from=${encodeURIComponent(dateFrom)}&date_to=${encodeURIComponent(dateTo)}&direction=ALL`;
  });
  ['historyInCards','historyOutCards'].forEach(id => {
    if ($(id)) $(id).addEventListener('click', (e) => {
      const card = e.target.closest('[data-move-product]');
      if (!card) return;
      openMovementHistory(card.dataset.moveProduct, card.dataset.moveDirection || 'ALL', 1);
    });
  });
  if ($('btnMovementHistorySearch')) $('btnMovementHistorySearch').addEventListener('click', () => loadMovementHistoryPage(1));

  document.querySelectorAll('[data-locreq-mode]').forEach(btn => {
    btn.addEventListener('click', () => {
      document.querySelectorAll('[data-locreq-mode]').forEach(x => x.classList.remove('active'));
      btn.classList.add('active');
      loadLocRequests(btn.dataset.locreqMode || 'received');
    });
  });

  if ($('btnLoadSalesRoutes')) $('btnLoadSalesRoutes').addEventListener('click', loadSalesCreatedOrders);
  if ($('salesRouteDate')) $('salesRouteDate').addEventListener('change', loadSalesCreatedOrders);
  if ($('salesRouteCards')) $('salesRouteCards').addEventListener('click', (e) => {
    const card = e.target.closest('[data-route-card]');
    if (!card) return;
    openRouteOrders(card.dataset.routeCard || '0');
  });

  document.addEventListener('click', (e) => {
    const bDel = e.target.closest('[data-del]');
    if (bDel) {
      const id = parseInt(bDel.dataset.del || '0', 10);
      if (id > 0) delMove(id);
    }

    const bSales = e.target.closest('[data-sales-detail]');
    if (bSales) {
      const id = parseInt(bSales.dataset.salesDetail || '0', 10);
      if (id > 0) openSalesOrderDetail(id);
    }

    const bBreportSales = e.target.closest('[data-breport-sales-detail]');
    if (bBreportSales) {
      const id = parseInt(bBreportSales.dataset.breportSalesDetail || '0', 10);
      if (id > 0) { if (routeOrdersModal) routeOrdersModal.hide(); openBreportSalesOrderDetail(id); }
    }

    const bLocReq = e.target.closest('[data-locreq-detail]');
    if (bLocReq) {
      const id = parseInt(bLocReq.dataset.locreqDetail || '0', 10);
      if (id > 0) openLocReqDetail(id);
    }
  });
});
</script>
</body>
</html>