<?php
declare(strict_types=1);

/**
 * ventas_pedidos.php
 * ============================================================
 * BReport · VENTAS BOTELLA · PEDIDOS Y EMBARQUES
 * ------------------------------------------------------------
 * Objetivo:
 * - Registrar pedidos logísticos de Botella utilizando clientes, rutas y embalajes configurados
 * - Ver detalle de artículos
 * - Cambiar estatus por parte del área de ventas
 * - Registrar bitácora de cambios en sales_order_status_log
 * - Consultar existencias disponibles por ubicación/planta
 * - Activar / desactivar clientes comerciales
 * - Activar / desactivar artículos
 *
 * Regla de negocio de inventario:
 * - BReport es MES multiempresa/multiplanta.
 * - Las existencias se consultan por empresa, planta/localidad y almacenes activos.
 * - Hielo Barra conserva reglas comerciales especiales cuando aplica.
 *
 * Notas:
 * - Este módulo vive en la raíz de BReport
 * - Ventas puede consultar inventario de todas las plantas
 *   activas de la empresa
 * ============================================================
 */

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

require_scope();
ensure_session();

$pdo = pdo();
$u   = current_user();
$sc  = scope();

$companyId        = (int)($sc['company_id'] ?? 0);
$currentLocationId = (int)($sc['location_id'] ?? 0);

/*
 * MÓDULO EXCLUSIVO BOTELLA
 * ------------------------------------------------------------
 * Botella cuenta con su propio flujo logístico de pedidos, empaques y rutas.
 * No debe ejecutar lógica de VentasHielo ni reutilizar condiciones de Hielo Barra.
 */
$stBotellaScope = $pdo->prepare("
    SELECT c.name AS company_name, l.name AS location_name
    FROM companies c
    JOIN locations l ON l.company_id = c.id AND l.id = ?
    WHERE c.id = ?
    LIMIT 1
");
$stBotellaScope->execute([$currentLocationId, $companyId]);
$botellaScope = $stBotellaScope->fetch(PDO::FETCH_ASSOC) ?: [];
$botellaCompanyName = (string)($botellaScope['company_name'] ?? '');
$botellaLocationName = (string)($botellaScope['location_name'] ?? '');
$botellaScopeText = mb_strtoupper($botellaCompanyName . ' ' . $botellaLocationName, 'UTF-8');

if (!str_contains($botellaScopeText, 'BOTELLA')) {
    http_response_code(403);
    exit('Este módulo corresponde únicamente a la empresa Botella. Regresa al Dashboard y selecciona la planta correspondiente.');
}

$canAccessVentas = has_role('admin')
    || has_role('sales')
    || has_role('ventas')
    || has_role('reparto')
    || has_role('supervisor')
    || has_role('corp')
    || has_role('plant_admin');

if (!$canAccessVentas) {
    http_response_code(403);
    exit('Acceso denegado. Este módulo requiere rol de ventas, reparto, supervisor o administrador.');
}

$canManage = has_role('admin')
    || has_role('sales')
    || has_role('ventas')
    || has_role('supervisor')
    || has_role('corp')
    || has_role('plant_admin');

$canSeeAllLocations = has_role('admin')
    || has_role('sales')
    || has_role('ventas')
    || has_role('supervisor')
    || has_role('corp')
    || has_role('plant_admin');

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

ensure_sales_module_schema($pdo);

/* ============================================================
   HELPERS
============================================================ */
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
{
    // Soporta JSON normal y FormData con __payload cuando se adjunta OC del cliente.
    if (isset($_POST['__payload'])) {
        $arr = json_decode((string)$_POST['__payload'], true);
        return is_array($arr) ? $arr : [];
    }
    $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 $arr, string $key, mixed $default = null): mixed
{
    return array_key_exists($key, $arr) ? $arr[$key] : $default;
}

/**
 * Las presentaciones comerciales se administran en piezas enteras.
 * No se redondea una captura del usuario: si viene decimal se rechaza para
 * impedir movimientos OUT fraccionarios en inventory_moves.
 */
function sales_is_whole_units(mixed $value): bool
{
    $qty = (float)$value;
    return abs($qty - round($qty)) <= 0.000001;
}

function sales_require_whole_units(mixed $value, string $label = 'Cantidad'): int
{
    $qty = (float)$value;
    if ($qty <= 0) {
        throw new RuntimeException($label . ' debe ser mayor a cero.');
    }
    if (!sales_is_whole_units($qty)) {
        throw new RuntimeException($label . ' solo permite piezas enteras; no se aceptan decimales.');
    }
    return (int)round($qty);
}

/**
 * Disponible comercial en piezas completas por almacén surtible.
 * Si existen residuos fraccionarios históricos, no se usan para surtir una
 * pieza nueva; deben corregirse mediante un ajuste real de inventario.
 */
function sales_available_whole_units(PDO $pdo, int $companyId, int $locationId, int $productId, string $sku): int
{
    $stockByWh = sales_product_warehouse_stock($pdo, $companyId, $locationId);
    $total = 0;
    foreach (sales_rule_warehouses_for_product($pdo, $companyId, $locationId, $productId, $sku, true) as $w) {
        $wid = (int)($w['warehouse_id'] ?? 0);
        $available = (float)($stockByWh[$productId][$wid] ?? 0);
        if ($available > 0) {
            $total += (int)floor($available + 0.000001);
        }
    }
    return $total;
}


/**
 * Botella controla inventario en botellas/piezas individuales.
 * La operación comercial se captura por paquetes completos dentro de una
 * tarima; el total de tarimas debe ser entero para liberar la orden.
 */
function botella_route_row(PDO $pdo, int $companyId, int $locationId, int $routeNo): array
{
    $st = $pdo->prepare("
        SELECT id, route_no, route_code, route_name, vehicle_unit, driver_name
        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('Selecciona una ruta activa configurada en Admin para esta planta.');
    }
    return $row;
}

function botella_packaging_row(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, 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;
    }

    /*
     * Fallback: products tiene pieces_per_package/packages_per_pallet.
     * Sirve para operar si aún no existe la opción en product_packaging_options.
     */
    $st = $pdo->prepare("
        SELECT
            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,
            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('Selecciona un embalaje activo configurado en Admin para el artículo.');
    }
    $row['id'] = 0;
    return $row;
}

function botella_validate_pallets(array $packaging, float $palletsQtyRaw, string $sku): array
{
    if ($palletsQtyRaw <= 0) {
        throw new RuntimeException('La cantidad de tarimas debe ser mayor a cero 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. Revisa antes de generar la orden.'
        );
    }

    $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('El embalaje de ' . $sku . ' no cuenta con configuración válida.');
    }

    $packagesQty = $palletsQty * $packagesPerPallet;

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

function status_label(string $status): string
{
    return match (strtoupper($status)) {
        'PENDIENTE'   => 'Pendiente',
        'EN_REVISION' => 'En revisión',
        'AUTORIZADA'  => 'Autorizada',
        'RECHAZADA'   => 'Rechazada',
        'CANCELADA'   => 'Cancelada',
        'SURTIENDO'   => 'Surtiendo',
        'EN_RUTA'     => 'En ruta',
        'ENTREGADA'   => 'Entregada',
        default       => $status,
    };
}

function allowed_sales_statuses(): array
{
    return [
        'PENDIENTE',
        'EN_REVISION',
        'AUTORIZADA',
        'RECHAZADA',
        'CANCELADA',
        'SURTIENDO',
        'EN_RUTA',
        'ENTREGADA',
    ];
}

function allowed_sales_actions_from_ui(): array
{
    return [
        'EN_REVISION',
        'AUTORIZADA',
        'RECHAZADA',
        'CANCELADA',
    ];
}

function can_transition(string $oldStatus, string $newStatus): bool
{
    $oldStatus = strtoupper(trim($oldStatus));
    $newStatus = strtoupper(trim($newStatus));

    if ($oldStatus === $newStatus) {
        return false;
    }

    if (!in_array($newStatus, allowed_sales_actions_from_ui(), true)) {
        return false;
    }

    $map = [
        'PENDIENTE'   => ['EN_REVISION', 'AUTORIZADA', 'RECHAZADA', 'CANCELADA'],
        'EN_REVISION' => ['AUTORIZADA', 'RECHAZADA', 'CANCELADA'],
        'AUTORIZADA'  => ['CANCELADA'],
        'RECHAZADA'   => [],
        'CANCELADA'   => [],
        'SURTIENDO'   => [],
        'EN_RUTA'     => [],
        'ENTREGADA'   => [],
    ];

    return in_array($newStatus, $map[$oldStatus] ?? [], true);
}

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

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

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 clamp_non_negative(float $value): float
{
    return $value < 0 ? 0.0 : $value;
}

function normalize_phone(string $phone): string
{
    $phone = trim($phone);
    $phone = preg_replace('/[^0-9+]/', '', $phone) ?? '';
    return substr($phone, 0, 30);
}

function customer_belongs_to_company(PDO $pdo, int $customerId, int $companyId): bool
{
    $st = $pdo->prepare("SELECT id FROM sales_customers WHERE id = ? AND company_id = ? LIMIT 1");
    $st->execute([$customerId, $companyId]);
    return (bool)$st->fetchColumn();
}


function sales_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 sales_table_exists(PDO $pdo, string $table): bool
{
    $st = $pdo->prepare("
        SELECT COUNT(*)
        FROM information_schema.tables
        WHERE table_schema = DATABASE()
          AND table_name = ?
    ");
    $st->execute([$table]);
    return (int)$st->fetchColumn() > 0;
}


function sales_forklift_select_sql(PDO $pdo, string $orderAlias = 'o'): string
{
    /*
     * Ventas muestra el mismo estado visual que Almacén Botella.
     * Si aún no existe la tabla de montacargas en una instalación antigua,
     * se regresan columnas NULL para no romper las consultas.
     */
    $orderAlias = preg_replace('/[^A-Za-z0-9_]/', '', $orderAlias) ?: 'o';
    if (!sales_table_exists($pdo, 'warehouse_forklift_load_orders')) {
        return ",
                NULL AS forklift_status,
                NULL AS forklift_order_id";
    }

    return ",
                (SELECT fo.status
                 FROM warehouse_forklift_load_orders fo
                 WHERE fo.company_id = {$orderAlias}.company_id
                   AND fo.location_id = {$orderAlias}.location_id
                   AND fo.order_id = {$orderAlias}.id
                 ORDER BY fo.id DESC
                 LIMIT 1) AS forklift_status,
                (SELECT fo.id
                 FROM warehouse_forklift_load_orders fo
                 WHERE fo.company_id = {$orderAlias}.company_id
                   AND fo.location_id = {$orderAlias}.location_id
                   AND fo.order_id = {$orderAlias}.id
                 ORDER BY fo.id DESC
                 LIMIT 1) AS forklift_order_id";
}

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

function sales_store_order_oc_attachment(PDO $pdo, int $companyId, int $locationId, int $orderId, string $purchaseOrder, array $file, int $userId, ?int $orderItemId = null, string $missingReason = ''): ?array
{
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return null;
    if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
        $GLOBALS['sales_upload_warnings'][] = 'No se pudo cargar el archivo de OC del cliente. La orden se creó sin adjunto.';
        return null;
    }
    $size = (int)($file['size'] ?? 0);
    if ($size <= 0) return null;
    if ($size > 8 * 1024 * 1024) {
        $GLOBALS['sales_upload_warnings'][] = 'El archivo de OC excede 8 MB. La orden se creó sin adjunto.';
        return null;
    }

    $original = (string)($file['name'] ?? 'oc_cliente');
    $ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
    if (!in_array($ext, sales_allowed_oc_attachment_ext(), true)) {
        $GLOBALS['sales_upload_warnings'][] = 'Formato de OC no permitido. Usa PDF, JPG, JPEG, PNG o WEBP. La orden se creó sin adjunto.';
        return null;
    }

    /*
     * La carpeta base debe existir previamente en el servidor.
     * No se crean carpetas desde PHP para evitar errores de permisos en hosting.
     * Ruta esperada: /breport/uploads/ordenes_clientes
     */
    $root = __DIR__ . '/uploads/ordenes_clientes';
    if (!is_dir($root)) {
        $GLOBALS['sales_upload_warnings'][] = 'La orden se creó, pero no se guardó el adjunto: no existe la carpeta /uploads/ordenes_clientes.';
        return null;
    }
    if (!is_writable($root)) {
        $GLOBALS['sales_upload_warnings'][] = 'La orden se creó, pero no se guardó el adjunto: /uploads/ordenes_clientes no tiene permiso de escritura para PHP/Apache.';
        return null;
    }

    $safePo = preg_replace('/[^A-Za-z0-9_\-]/', '_', $purchaseOrder !== '' ? $purchaseOrder : ('OC_' . $orderId));
    $safePo = trim((string)$safePo, '_') ?: ('OC_' . $orderId);
    $filename = $safePo . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(3)) . '.' . $ext;
    $dest = $root . '/' . $filename;
    if (!move_uploaded_file((string)$file['tmp_name'], $dest)) {
        $GLOBALS['sales_upload_warnings'][] = 'La orden se creó, pero no se pudo guardar el archivo de OC en el servidor.';
        return null;
    }
    @chmod($dest, 0664);

    $relative = 'uploads/ordenes_clientes/' . $filename;
    if (sales_table_exists($pdo, 'sales_order_attachments')) {
        $st = $pdo->prepare("
            INSERT INTO sales_order_attachments (
                company_id, location_id, order_id, order_item_id, purchase_order,
                original_name, stored_name, file_path, mime_type, file_size,
                uploaded_by, created_at
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
        ");
        $st->execute([
            $companyId, $locationId, $orderId, $orderItemId, ($purchaseOrder !== '' ? $purchaseOrder : null),
            mb_substr($original, 0, 180, 'UTF-8'), $filename, $relative,
            mb_substr((string)($file['type'] ?? ''), 0, 120, 'UTF-8'), $size, $userId
        ]);
    }
    return ['path'=>$relative, 'name'=>$original, 'size'=>$size];
}

function sales_pdf_url(int $orderId, int $outboundId = 0, string $folio = ''): string
{
    $url = './orden_entrega_pdf.php?order_id=' . $orderId;
    if ($outboundId > 0) {
        $url .= '&outbound_id=' . $outboundId;
    }
    if ($folio !== '') {
        $url .= '&folio=' . rawurlencode($folio);
    }
    return $url;
}



function sales_find_or_create_customer(PDO $pdo, int $companyId, int $locationId, string $businessName, string $phone = ''): int
{
    $businessName = trim($businessName);
    if ($businessName === '') {
        $businessName = 'Orden de salida Ventas';
    }
    $phone = normalize_phone($phone);

    $st = $pdo->prepare("
        SELECT id
        FROM sales_customers
        WHERE company_id = ?
          AND business_name = ?
          " . (sales_column_exists($pdo, 'sales_customers', 'location_id') ? "AND (location_id IS NULL OR location_id = ?)" : "") . "
        ORDER BY id DESC
        LIMIT 1
    ");
    $paramsFindCustomer = [$companyId, $businessName];
    if (sales_column_exists($pdo, 'sales_customers', 'location_id')) $paramsFindCustomer[] = $locationId;
    $st->execute($paramsFindCustomer);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id > 0) return $id;

    if (sales_column_exists($pdo, 'sales_customers', 'location_id')) {
        $st = $pdo->prepare("
            INSERT INTO sales_customers (company_id, location_id, business_name, contact_name, phone, active, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, 1, NOW(), NOW())
        ");
        $st->execute([$companyId, $locationId > 0 ? $locationId : null, $businessName, $businessName, $phone !== '' ? $phone : null]);
    } else {
        $st = $pdo->prepare("
            INSERT INTO sales_customers (company_id, business_name, contact_name, phone, active, created_at, updated_at)
            VALUES (?, ?, ?, ?, 1, NOW(), NOW())
        ");
        $st->execute([$companyId, $businessName, $businessName, $phone !== '' ? $phone : null]);
    }
    return (int)$pdo->lastInsertId();
}

function sales_validate_location(PDO $pdo, int $companyId, int $locationId): array
{
    $st = $pdo->prepare("
        SELECT id, name, code
        FROM locations
        WHERE id = ?
          AND company_id = ?
          AND active = 1
        LIMIT 1
    ");
    $st->execute([$locationId, $companyId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) {
        throw new RuntimeException('Ubicación inválida o inactiva.');
    }
    return $row;
}

function sales_product_lookup(PDO $pdo, int $companyId, array $productIds): array
{
    $productIds = array_values(array_unique(array_filter(array_map('intval', $productIds), fn($v) => $v > 0)));
    if (!$productIds) return [];

    $ph = implode(',', array_fill(0, count($productIds), '?'));
    $params = array_merge([$companyId], $productIds);
    $st = $pdo->prepare("
        SELECT id, sku, name, uom, active
        FROM products
        WHERE company_id = ?
          AND id IN ($ph)
    ");
    $st->execute($params);

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

function sales_default_channel_id(PDO $pdo, int $companyId): int
{
    if (!sales_table_exists($pdo, 'outbound_channels')) {
        return 0;
    }
    $st = $pdo->prepare("
        SELECT id
        FROM outbound_channels
        WHERE company_id = ? AND active = 1
        ORDER BY sort_order, id
        LIMIT 1
    ");
    $st->execute([$companyId]);
    return (int)($st->fetchColumn() ?: 0);
}


function sales_warehouse_id_by_code(PDO $pdo, int $companyId, int $locationId, string $code): int
{
    $st = $pdo->prepare("
        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 sales_product_warehouse_stock(PDO $pdo, int $companyId, int $locationId): array
{
    $st = $pdo->prepare("
        SELECT
            product_id,
            warehouse_id,
            COALESCE(SUM(CASE
                WHEN move_type IN ('IN','ADJ') THEN qty
                WHEN move_type = 'OUT' THEN -qty
                ELSE 0
            END),0) 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]);

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


function sales_location_context(PDO $pdo, int $companyId, int $locationId): array
{
    $companyName = '';
    $locationName = '';
    try {
        $st = $pdo->prepare("SELECT name FROM companies WHERE id=? LIMIT 1");
        $st->execute([$companyId]);
        $companyName = (string)($st->fetchColumn() ?: '');

        $st = $pdo->prepare("SELECT name FROM locations WHERE id=? AND company_id=? LIMIT 1");
        $st->execute([$locationId, $companyId]);
        $locationName = (string)($st->fetchColumn() ?: '');
    } catch (Throwable $e) {
        // No se bloquea ventas si falta descripción de alcance.
    }

    $label = mb_strtoupper(trim($companyName . ' ' . $locationName), 'UTF-8');
    $isHieloScope = (
        str_contains($label, 'HIELO')
        && str_contains($label, 'BARRA')
    );

    return [
        'company_name' => $companyName,
        'location_name' => $locationName,
        'is_hielo_scope' => $isHieloScope,
    ];
}

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

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

function sales_is_route_warehouse_code(string $code): bool
{
    /*
     * Botella maneja las rutas como almacenes móviles (RUTA-01, RUTA-02, etc.).
     * Esas existencias ya fueron cargadas a reparto y NO deben considerarse
     * inventario disponible para Ventas/Almacén, salvo que exista una devolución
     * real de la ruta al almacén físico.
     */
    return (bool)preg_match('/^RUTA[-_ ]?\d+$/i', trim($code));
}

function sales_active_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 && sales_is_disposal_warehouse_code($code)) {
            continue;
        }
        if (sales_is_route_warehouse_code($code)) {
            continue;
        }
        $rows[] = [
            'warehouse_id' => (int)$w['id'],
            'warehouse_code' => (string)$w['code'],
            'warehouse_name' => (string)$w['name'],
        ];
    }
    return $rows;
}

function sales_warehouse_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
    ");
    $st->execute([$companyId, $locationId]);

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

function sales_warehouse_rows_by_ids(PDO $pdo, int $companyId, int $locationId, array $warehouseIds, bool $excludeDisposal = true): array
{
    $warehouseIds = array_values(array_unique(array_filter(array_map('intval', $warehouseIds), fn($v) => $v > 0)));
    if (!$warehouseIds) return [];

    $ph = implode(',', array_fill(0, count($warehouseIds), '?'));
    $params = array_merge([$companyId, $locationId], $warehouseIds);

    $st = $pdo->prepare("
        SELECT id, code, name
        FROM warehouses
        WHERE company_id = ?
          AND location_id = ?
          AND active = 1
          AND id IN ($ph)
        ORDER BY FIELD(UPPER(code),'CF1','CF2','CF3','ALM01','CUB','SOB-CUB','CUA','MERMA','APROV-MERMA'), code, name
    ");
    $st->execute($params);

    $rows = [];
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $w) {
        $code = strtoupper(trim((string)$w['code']));
        if ($excludeDisposal && sales_is_disposal_warehouse_code($code)) {
            continue;
        }
        if (sales_is_route_warehouse_code($code)) {
            continue;
        }
        $rows[] = [
            'warehouse_id' => (int)$w['id'],
            'warehouse_code' => (string)$w['code'],
            'warehouse_name' => (string)$w['name'],
        ];
    }
    return $rows;
}

function sales_rule_warehouses_for_product(PDO $pdo, int $companyId, int $locationId, int $productId, string $sku, bool $forAvailability = true): array
{
    $sku = strtoupper(trim($sku));
    $ctx = sales_location_context($pdo, $companyId, $locationId);
    $ids = [];

    /*
     * Flujo exclusivo Hielo Barra:
     * - Barra 150 KILOS se consulta y surte únicamente desde CF1 / CF2.
     * - Producto terminado se consulta y surte únicamente desde CF3.
     * - CUB / SOB-CUB no son existencia comercial de venta.
     *
     * Esta regla se prioriza incluso si existe una configuración incorrecta,
     * para no afectar la operación funcional del flujo de Hielo Barra.
     */
    if (!empty($ctx['is_hielo_scope'])) {
        $preferredCodes = ($sku === 'BARRA 150 KILOS') ? ['CF1', 'CF2'] : ['CF3'];
        $preferredIds = [];
        foreach ($preferredCodes as $code) {
            $wid = sales_warehouse_id_by_code($pdo, $companyId, $locationId, $code);
            if ($wid > 0) $preferredIds[] = $wid;
        }
        return sales_warehouse_rows_by_ids($pdo, $companyId, $locationId, $preferredIds, true);
    }

    /*
     * Flujo MES de Botella, Hielo Bolsa, Garrafón y nuevas plantas:
     * primero respeta la configuración producto/localidad; si aún no existe,
     * toma los almacenes operativos activos creados para esa localidad.
     */
    if (sales_table_exists($pdo, 'product_warehouse_rules')) {
        $selectCols = "default_finished_warehouse_id, default_sales_warehouse_id";
        if (sales_column_exists($pdo, 'product_warehouse_rules', 'cubicado_warehouse_id')) {
            $selectCols .= ", cubicado_warehouse_id";
        } else {
            $selectCols .= ", NULL AS cubicado_warehouse_id";
        }

        $st = $pdo->prepare("
            SELECT {$selectCols}
            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) {
            $ids[] = (int)($rule['default_sales_warehouse_id'] ?? 0);
            $ids[] = (int)($rule['default_finished_warehouse_id'] ?? 0);
            if (!$forAvailability) {
                $ids[] = (int)($rule['cubicado_warehouse_id'] ?? 0);
            }

            $rows = sales_warehouse_rows_by_ids($pdo, $companyId, $locationId, $ids, true);
            if ($rows) return $rows;
        }
    }

    return sales_active_warehouses($pdo, $companyId, $locationId, true);
}

function sales_inventory_note(PDO $pdo, int $companyId, int $locationId): string
{
    $ctx = sales_location_context($pdo, $companyId, $locationId);
    if (!empty($ctx['is_hielo_scope'])) {
        return 'Inventario de venta para Hielo Barra: barra comercial CF1/CF2 y producto terminado según reglas de almacén.';
    }
    return 'Inventario de venta MES: existencias por almacenes físicos activos de la planta seleccionada. Las rutas móviles RUTA-## no se consideran disponibles para nueva venta.';
}

function sales_allocate_inventory_out(PDO $pdo, int $companyId, int $locationId, int $productId, string $sku, float $qty): array
{
    /**
     * Asignación real de salida para pedidos de Ventas.
     * - Respeta empresa + planta/localidad del pedido.
     * - Usa product_warehouse_rules cuando existen.
     * - Si no hay regla, toma almacenes activos operativos de esa planta.
     * - Hielo Barra conserva compatibilidad comercial CF1/CF2 para barra y CF3 para terminado.
     */
    $qty = (float)sales_require_whole_units($qty, 'Cantidad a surtir');
    $sku = strtoupper(trim($sku));
    $stockByWh = sales_product_warehouse_stock($pdo, $companyId, $locationId);
    $candidateRows = sales_rule_warehouses_for_product($pdo, $companyId, $locationId, $productId, $sku, true);

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

    foreach ($candidateRows as $w) {
        $wid = (int)($w['warehouse_id'] ?? 0);
        if ($wid <= 0) continue;

        // Solo piezas completas pueden generar impacto real de salida.
        $available = (float)floor(max(0, (float)($stockByWh[$productId][$wid] ?? 0.0)) + 0.000001);
        if ($available <= 0) continue;

        $take = min($remaining, $available);
        if ($take > 0) {
            $alloc[] = [
                'warehouse_id' => $wid,
                'warehouse_code' => (string)($w['warehouse_code'] ?? ''),
                'qty' => round($take, 3),
            ];
            $remaining = round($remaining - $take, 3);
        }

        if ($remaining <= 0.0001) break;
    }

    if ($remaining > 0.0001) {
        $labels = array_filter(array_map(fn($w) => (string)($w['warehouse_code'] ?? ''), $candidateRows));
        $label = $labels ? implode('/', $labels) : 'almacenes activos de la planta';
        throw new RuntimeException(
            'Inventario insuficiente en ' . $label .
            ' para producto ID ' . $productId .
            '. Faltante: ' . number_format($remaining, 0, '.', '')
        );
    }

    return $alloc;
}

function ensure_sales_module_schema(PDO $pdo): void
{
    try {
        if (sales_table_exists($pdo, 'sales_orders') && !sales_column_exists($pdo, 'sales_orders', 'route_no')) {
            $pdo->exec("ALTER TABLE sales_orders ADD COLUMN route_no TINYINT UNSIGNED DEFAULT NULL AFTER requested_delivery_time");
        }
    } catch (Throwable $e) {}

    try {
        if (sales_table_exists($pdo, 'sales_orders') && !sales_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 (sales_table_exists($pdo, 'sales_customers') && !sales_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 (sales_table_exists($pdo, 'sales_customers') && !sales_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 (sales_table_exists($pdo, 'sales_customer_addresses') && !sales_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 (sales_table_exists($pdo, 'sales_order_items') && !sales_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");
        }
    } catch (Throwable $e) {}

    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_column_exists($pdo, 'sales_order_items', 'oc_relation')) {
            $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN oc_relation VARCHAR(60) DEFAULT NULL AFTER purchase_order");
        }
    } catch (Throwable $e) {}

    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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 oc_relation");
        }
    } catch (Throwable $e) {}

    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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");
        }
    } catch (Throwable $e) {}

    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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");
        }
    } catch (Throwable $e) {}

    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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");
        }
    } catch (Throwable $e) {}

    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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");
        }
    } catch (Throwable $e) {}

    // Columnas para trazabilidad por OC/cliente/dirección en OSV. Para OSA quedan NULL.
    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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");
        }
    } catch (Throwable $e) {}
    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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");
        }
    } catch (Throwable $e) {}
    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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");
        }
    } catch (Throwable $e) {}
    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_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) {}
    try {
        if (sales_table_exists($pdo, 'sales_order_items') && !sales_column_exists($pdo, 'sales_order_items', 'oc_attachment_missing_reason')) {
            $pdo->exec("ALTER TABLE sales_order_items ADD COLUMN oc_attachment_missing_reason VARCHAR(255) DEFAULT NULL AFTER delivery_address");
        }
    } catch (Throwable $e) {}

    try {
        if (!sales_table_exists($pdo, 'sales_order_attachments')) {
            $pdo->exec("CREATE TABLE sales_order_attachments (
                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,
                order_item_id BIGINT UNSIGNED DEFAULT NULL,
                purchase_order VARCHAR(80) DEFAULT NULL,
                original_name VARCHAR(180) NOT NULL,
                stored_name VARCHAR(180) NOT NULL,
                file_path VARCHAR(255) NOT NULL,
                mime_type VARCHAR(120) DEFAULT NULL,
                file_size BIGINT UNSIGNED DEFAULT NULL,
                missing_reason VARCHAR(255) DEFAULT NULL,
                uploaded_by BIGINT UNSIGNED DEFAULT NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY(id),
                KEY idx_soa_order(order_id),
                KEY idx_soa_scope(company_id, location_id),
                KEY idx_soa_po(purchase_order)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
        }
    } catch (Throwable $e) {}
    try {
        if (sales_table_exists($pdo, 'sales_order_attachments') && !sales_column_exists($pdo, 'sales_order_attachments', 'missing_reason')) {
            $pdo->exec("ALTER TABLE sales_order_attachments ADD COLUMN missing_reason VARCHAR(255) DEFAULT NULL AFTER file_size");
        }
    } catch (Throwable $e) {}
}

function sales_route_label(?int $routeNo): string
{
    return ($routeNo !== null && $routeNo >= 1 && $routeNo <= 20) ? ('Ruta ' . $routeNo) : '';
}

function sales_route_warehouse_code(int $routeNo): string
{
    return 'RUTA-' . str_pad((string)$routeNo, 2, '0', STR_PAD_LEFT);
}

function sales_route_stock_rows(PDO $pdo, int $companyId, int $locationId, int $routeNo): array
{
    if ($routeNo <= 0) return [];

    $warehouseCode = sales_route_warehouse_code($routeNo);
    $stWh = $pdo->prepare("
        SELECT id
        FROM warehouses
        WHERE company_id = ?
          AND location_id = ?
          AND active = 1
          AND UPPER(code) = UPPER(?)
        LIMIT 1
    ");
    $stWh->execute([$companyId, $locationId, $warehouseCode]);
    $warehouseId = (int)($stWh->fetchColumn() ?: 0);
    if ($warehouseId <= 0) return [];

    $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, $warehouseId, $companyId]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}

function sales_route_conflict(PDO $pdo, int $companyId, int $locationId, string $deliveryDate, int $routeNo, int $excludeOrderId = 0): ?array
{
    if ($routeNo < 1 || $routeNo > 20 || !sales_column_exists($pdo, 'sales_orders', 'route_no')) return null;
    $st = $pdo->prepare("\n        SELECT id, sales_order_ref, status\n        FROM sales_orders\n        WHERE company_id = ? AND location_id = ? AND requested_delivery_date = ? AND route_no = ? AND id <> ?\n          AND status NOT IN ('CANCELADA','RECHAZADA','ENTREGADA')\n        ORDER BY id DESC\n        LIMIT 1\n    ");
    $st->execute([$companyId, $locationId, $deliveryDate, $routeNo, $excludeOrderId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
}

/**
 * Snapshot de inventario para consulta de ventas.
 * Regla:
 * - Barra comercial = solo CF1 + CF2
 * - CUB puede recibir barra para transformación, pero NO cuenta como inventario comercial
 * - CF3 almacena artículos convertidos, NO barra comercial
 */
function build_inventory_snapshot(PDO $pdo, int $companyId, int $locationId): array
{
    /**
     * Snapshot de inventario disponible para Ventas.
     * Fuente única oficial: inventory_moves.
     *
     * BReport no debe limitar ventas a CF1/CF2/CF3 para todas las plantas.
     * Cada pedido se consulta/surte con empresa + planta/localidad + almacenes
     * activos o reglas product_warehouse_rules de esa localidad.
     */
    $barId = get_bar_product_id($pdo, $companyId);
    $whMap = sales_warehouse_map($pdo, $companyId, $locationId);

    $st = $pdo->prepare("
        SELECT id, sku, name, uom, active
        FROM products
        WHERE company_id = ?
        ORDER BY sku
    ");
    $st->execute([$companyId]);
    $products = $st->fetchAll(PDO::FETCH_ASSOC);

    $rows = [];
    $allowedByProduct = [];

    foreach ($products as $p) {
        $pid = (int)$p['id'];
        $sku = (string)$p['sku'];
        $allowedRows = sales_rule_warehouses_for_product($pdo, $companyId, $locationId, $pid, $sku, true);
        $allowedIds = [];
        foreach ($allowedRows as $w) {
            $wid = (int)($w['warehouse_id'] ?? 0);
            if ($wid > 0) $allowedIds[$wid] = true;
        }
        $allowedByProduct[$pid] = $allowedIds;

        $rows[$pid] = [
            'product_id'  => $pid,
            'sku'         => $sku,
            'name'        => (string)$p['name'],
            'uom'         => (string)$p['uom'],
            'active'      => (int)$p['active'],
            'prod_cf1'    => 0.0,
            'prod_cf2'    => 0.0,
            'prod_cf3'    => 0.0,
            'prod_cub'    => 0.0,
            'cubing_in'   => 0.0,
            'quality_out' => 0.0,
            'manual_in'   => 0.0,
            'manual_out'  => 0.0,
            'on_hand'     => 0.0,
            'warehouse_breakdown' => [],
            'inventory_note' => sales_inventory_note($pdo, $companyId, $locationId),
        ];
    }

    $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 im.company_id = ?
          AND im.location_id = ?
          AND im.warehouse_id IS NOT NULL
        GROUP BY im.product_id, im.warehouse_id, UPPER(im.move_type)
    ");
    $st->execute([$companyId, $locationId]);

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

        $wid  = (int)($r['warehouse_id'] ?? 0);
        $qty  = (float)$r['qty'];
        $typ  = strtoupper(trim((string)$r['move_type']));
        $code = (string)($whMap[$wid]['code_upper'] ?? '');
        $signed = ($typ === 'IN' || $typ === 'ADJ') ? $qty : (($typ === 'OUT') ? -$qty : 0.0);

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

        if (sales_is_route_warehouse_code($code)) {
            // Existencia cargada a reparto: no se muestra ni suma como disponible comercial.
            continue;
        }

        if (!isset($allowedByProduct[$pid][$wid])) {
            continue;
        }

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

        $rows[$pid]['warehouse_breakdown'][$wid]['qty'] += $signed;
        $rows[$pid]['on_hand'] += $signed;

        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'],
            fn($w) => abs((float)($w['qty'] ?? 0)) > 0.0001
        ));
        usort($row['warehouse_breakdown'], fn($a, $b) => strcmp((string)$a['warehouse_code'], (string)$b['warehouse_code']));
    }
    unset($row);

    return [
        'rows' => array_values($rows),
        'bar_product_id' => $barId,
        'inventory_note' => sales_inventory_note($pdo, $companyId, $locationId),
    ];
}

/* ============================================================
   API
============================================================ */
if ($action !== '') {
    need_csrf();


    /*
     * BANDEJA COMERCIAL MES · SOLO CONSULTA
     * ------------------------------------------------------------
     * Devuelve pedidos por planta/localidad, sin costos, para la vista de
     * bandeja. No modifica el flujo existente de autorización o surtido.
     */
    if ($action === 'orders_mailbox') {
        $selectedLocationId = (int)($_GET['location_id'] ?? 0);
        $dateFrom = trim((string)($_GET['date_from'] ?? ''));
        $dateTo = trim((string)($_GET['date_to'] ?? ''));
        $customer = trim((string)($_GET['customer'] ?? ''));

        $where = ["o.company_id = ?"];
        $params = [$companyId];

        if (!$canSeeAllLocations) {
            $where[] = "o.location_id = ?";
            $params[] = $currentLocationId;
        } elseif ($selectedLocationId > 0) {
            $where[] = "o.location_id = ?";
            $params[] = $selectedLocationId;
        } else {
            // Por seguridad operativa la bandeja inicia con el scope actual.
            $where[] = "o.location_id = ?";
            $params[] = $currentLocationId;
        }

        if ($dateFrom !== '') {
            $where[] = "o.requested_delivery_date >= ?";
            $params[] = $dateFrom;
        }
        if ($dateTo !== '') {
            $where[] = "o.requested_delivery_date <= ?";
            $params[] = $dateTo;
        }
        if ($customer !== '') {
            $where[] = "(c.business_name LIKE ? OR c.contact_name LIKE ? OR o.sales_order_ref LIKE ?)";
            $params[] = '%' . $customer . '%';
            $params[] = '%' . $customer . '%';
            $params[] = '%' . $customer . '%';
        }

        $forkliftSelectSql = sales_forklift_select_sql($pdo, 'o');

        $sql = "
            SELECT
                o.id,
                o.location_id,
                o.sales_order_ref,
                o.requested_delivery_date,
                o.requested_delivery_time,
                o.route_no,
                o.delivery_address,
                o.status,
                o.source,
                o.outbound_id,
                o.created_at
                {$forkliftSelectSql},
                c.business_name,
                c.contact_name,
                c.phone,
                l.name AS location_name,
                l.code AS location_code,
                COUNT(soi.id) AS items_count,
                COALESCE(SUM(soi.qty), 0) AS total_qty
            FROM sales_orders o
            JOIN sales_customers c ON c.id = o.customer_id
            JOIN locations l ON l.id = o.location_id
            LEFT JOIN sales_order_items soi ON soi.order_id = o.id
            WHERE " . implode(' AND ', $where) . "
            GROUP BY
                o.id, o.location_id, o.sales_order_ref, o.requested_delivery_date,
                o.requested_delivery_time, o.route_no, o.delivery_address, o.status,
                o.source, o.outbound_id, o.created_at, c.business_name, c.contact_name,
                c.phone, l.name, l.code
            ORDER BY o.requested_delivery_date DESC, o.id DESC
            LIMIT 500
        ";
        $st = $pdo->prepare($sql);
        $st->execute($params);
        jexit(['ok'=>true, 'rows'=>$st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    /*
     * SUGERENCIAS INTELIGENTES COMERCIALES · SOLO CONSULTA
     * ------------------------------------------------------------
     * Analiza pedidos y existencias surtibles de la planta seleccionada.
     * La existencia usa build_inventory_snapshot(), por lo que conserva:
     * - Hielo Barra: CF1/CF2 para barra y CF3 para terminado.
     * - Otras plantas: sus reglas y almacenes activos por localidad.
     */
    if ($action === 'sales_insights') {
        $selectedLocationId = (int)($_GET['location_id'] ?? 0);
        if (!$canSeeAllLocations || $selectedLocationId <= 0) {
            $selectedLocationId = $currentLocationId;
        }
        $loc = sales_validate_location($pdo, $companyId, $selectedLocationId);

        $dateTo = trim((string)($_GET['date_to'] ?? date('Y-m-d')));
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) $dateTo = date('Y-m-d');
        $dateFrom = trim((string)($_GET['date_from'] ?? ''));
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) {
            $dateFrom = (new DateTimeImmutable($dateTo))->modify('-6 days')->format('Y-m-d');
        }
        if ($dateFrom > $dateTo) {
            $tmp = $dateFrom; $dateFrom = $dateTo; $dateTo = $tmp;
        }
        $days = max(1, (int)(new DateTimeImmutable($dateFrom))->diff(new DateTimeImmutable($dateTo))->days + 1);

        $st = $pdo->prepare("
            SELECT
                COUNT(DISTINCT o.id) AS orders_total,
                SUM(CASE WHEN o.status='AUTORIZADA' AND o.outbound_id IS NULL THEN 1 ELSE 0 END) AS pending_dispatch,
                SUM(CASE WHEN o.status IN ('PENDIENTE','EN_REVISION') THEN 1 ELSE 0 END) AS pending_review,
                SUM(CASE WHEN o.status IN ('SURTIENDO','EN_RUTA','ENTREGADA') OR o.outbound_id IS NOT NULL THEN 1 ELSE 0 END) AS fulfilled
            FROM sales_orders o
            WHERE o.company_id=?
              AND o.location_id=?
              AND o.requested_delivery_date BETWEEN ? AND ?
        ");
        $st->execute([$companyId, $selectedLocationId, $dateFrom, $dateTo]);
        $summary = $st->fetch(PDO::FETCH_ASSOC) ?: [];

        $st = $pdo->prepare("
            SELECT
                p.id AS product_id, p.sku, p.name AS product_name, p.uom,
                COALESCE(SUM(soi.qty),0) AS requested_qty,
                COUNT(DISTINCT o.id) AS order_count
            FROM sales_orders o
            JOIN sales_order_items soi ON soi.order_id=o.id
            JOIN products p ON p.id=soi.product_id
            WHERE o.company_id=?
              AND o.location_id=?
              AND o.status='AUTORIZADA'
              AND o.outbound_id IS NULL
            GROUP BY p.id, p.sku, p.name, p.uom
            ORDER BY requested_qty DESC
        ");
        $st->execute([$companyId, $selectedLocationId]);
        $pendingProducts = $st->fetchAll(PDO::FETCH_ASSOC);

        $snapshot = build_inventory_snapshot($pdo, $companyId, $selectedLocationId);
        $stockMap = [];
        foreach (($snapshot['rows'] ?? []) as $row) {
            $stockMap[(int)$row['product_id']] = (float)$row['on_hand'];
        }

        $risks = [];
        foreach ($pendingProducts as &$product) {
            $requested = (float)$product['requested_qty'];
            $available = (float)($stockMap[(int)$product['product_id']] ?? 0);
            $shortage = max(0.0, $requested - $available);
            $product['requested_qty'] = round($requested, 3);
            $product['available'] = round($available, 3);
            $product['shortage'] = round($shortage, 3);
            if ($shortage > 0.0001 || $available <= 0.0001) {
                $product['risk_level'] = $available <= 0.0001 ? 'critical' : 'warning';
                $risks[] = $product;
            }
        }
        unset($product);

        $st = $pdo->prepare("
            SELECT
                c.id AS customer_id,
                c.business_name,
                c.contact_name,
                COUNT(DISTINCT o.id) AS order_count,
                COALESCE(SUM(soi.qty),0) AS qty_requested
            FROM sales_orders o
            JOIN sales_customers c ON c.id=o.customer_id
            LEFT JOIN sales_order_items soi ON soi.order_id=o.id
            WHERE o.company_id=?
              AND o.location_id=?
              AND o.requested_delivery_date BETWEEN ? AND ?
              AND o.status NOT IN ('RECHAZADA','CANCELADA')
            GROUP BY c.id, c.business_name, c.contact_name
            ORDER BY order_count DESC, qty_requested DESC
            LIMIT 5
        ");
        $st->execute([$companyId, $selectedLocationId, $dateFrom, $dateTo]);
        $topCustomers = $st->fetchAll(PDO::FETCH_ASSOC);

        $recommendations = [];
        $pendingDispatch = (int)($summary['pending_dispatch'] ?? 0);
        $pendingReview = (int)($summary['pending_review'] ?? 0);

        if ($pendingDispatch > 0) {
            $recommendations[] = [
                'level'=>'info',
                'title'=>'Pedidos autorizados por surtir',
                'text'=>$pendingDispatch . ' pedido(s) autorizados esperan salida en ' . (string)$loc['name'] . '. Coordina con Almacén.'
            ];
        }
        if ($risks) {
            $first = $risks[0];
            $recommendations[] = [
                'level'=>($first['risk_level'] ?? 'warning'),
                'title'=>'Riesgo de surtido',
                'text'=>(string)$first['sku'] . ' requiere ' . number_format((float)$first['requested_qty'], 0, '.', '') .
                    ' y dispone de ' . number_format((float)$first['available'], 0, '.', '') .
                    '. Faltante: ' . number_format((float)$first['shortage'], 0, '.', '') . '.'
            ];
        }
        if ($pendingReview > 0) {
            $recommendations[] = [
                'level'=>'warning',
                'title'=>'Pedidos pendientes de revisión',
                'text'=>$pendingReview . ' pedido(s) requieren autorización, rechazo o cancelación para no detener la planeación.'
            ];
        }
        if ($topCustomers) {
            $top = $topCustomers[0];
            $name = trim((string)($top['business_name'] ?: $top['contact_name']));
            $recommendations[] = [
                'level'=>'success',
                'title'=>'Cliente con mayor actividad',
                'text'=>$name . ' registra ' . (int)$top['order_count'] . ' pedido(s) en el periodo por ' .
                    number_format((float)$top['qty_requested'], 0, '.', '') . ' piezas.'
            ];
        }
        if (!$recommendations) {
            $recommendations[] = [
                'level'=>'success',
                'title'=>'Operación comercial estable',
                'text'=>'No se detectan pedidos pendientes ni faltantes de inventario surtible en el periodo seleccionado.'
            ];
        }

        jexit([
            'ok'=>true,
            'location'=>$loc,
            'filters'=>['date_from'=>$dateFrom, 'date_to'=>$dateTo, 'days'=>$days],
            'summary'=>[
                'orders_total'=>(int)($summary['orders_total'] ?? 0),
                'pending_dispatch'=>$pendingDispatch,
                'pending_review'=>$pendingReview,
                'fulfilled'=>(int)($summary['fulfilled'] ?? 0),
                'risks'=>count($risks),
            ],
            'risks'=>array_slice($risks, 0, 5),
            'top_customers'=>$topCustomers,
            'recommendations'=>$recommendations,
            'inventory_note'=>$snapshot['inventory_note'] ?? sales_inventory_note($pdo, $companyId, $selectedLocationId),
        ]);
    }

    if ($action === 'orders') {
        $status            = trim((string)($_GET['status'] ?? ''));
        $dateFrom          = trim((string)($_GET['date_from'] ?? ''));
        $dateTo            = trim((string)($_GET['date_to'] ?? ''));
        $customer          = trim((string)($_GET['customer'] ?? ''));
        $selectedLocationId = (int)($_GET['location_id'] ?? 0);
        $view = strtolower(trim((string)($_GET['view'] ?? 'active')));

        $where = ["o.company_id = ?"];
        $params = [$companyId];

        if (!$GLOBALS['canSeeAllLocations']) {
            $where[] = "o.location_id = ?";
            $params[] = $GLOBALS['currentLocationId'];
        } elseif ($selectedLocationId > 0) {
            $where[] = "o.location_id = ?";
            $params[] = $selectedLocationId;
        }

        if ($status !== '') {
            $where[] = "o.status = ?";
            $params[] = $status;
        }

        if ($dateFrom !== '') {
            $where[] = "o.requested_delivery_date >= ?";
            $params[] = $dateFrom;
        }

        if ($dateTo !== '') {
            $where[] = "o.requested_delivery_date <= ?";
            $params[] = $dateTo;
        }

        if ($customer !== '') {
            $where[] = "(c.business_name LIKE ? OR c.contact_name LIKE ?)";
            $params[] = '%' . $customer . '%';
            $params[] = '%' . $customer . '%';
        }

        if ($view === 'history') {
            $where[] = "(o.outbound_id IS NOT NULL OR o.status IN ('SURTIENDO','EN_RUTA','ENTREGADA'))";
            $where[] = "(COALESCE(o.source,'') IN ('VentasHielo','VENTASHIELO','VH') OR o.sales_order_ref LIKE 'PW-%' OR o.outbound_id IS NOT NULL)";
        } elseif ($view === 'active') {
            $where[] = "(o.outbound_id IS NULL AND o.status NOT IN ('SURTIENDO','EN_RUTA','ENTREGADA'))";
        }

        $forkliftSelectSql = sales_forklift_select_sql($pdo, 'o');

        $sql = "
            SELECT
                o.id,
                o.location_id,
                o.sales_order_ref,
                o.requested_delivery_date,
                o.requested_delivery_time,
                o.route_no,
                o.delivery_address,
                o.status,
                o.total,
                o.source,
                o.comments,
                o.outbound_id,
                o.created_at
                {$forkliftSelectSql},
                c.id AS customer_id,
                c.business_name,
                c.contact_name,
                c.phone,
                cu.full_name AS customer_user_name,
                l.name AS location_name,
                l.code AS location_code,
                (
                    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
            LEFT JOIN sales_customer_users cu
              ON cu.id = o.customer_user_id
            LEFT JOIN locations l
              ON l.id = o.location_id
            WHERE " . implode(' AND ', $where) . "
            ORDER BY o.id DESC
            LIMIT 400
        ";

        $st = $pdo->prepare($sql);
        $st->execute($params);

        jexit([
            'ok' => true,
            'rows' => $st->fetchAll(PDO::FETCH_ASSOC),
            'can_manage' => $canManage,
        ]);
    }

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

        $forkliftSelectSql = sales_forklift_select_sql($pdo, 'o');

        $sql = "
            SELECT
                o.*
                {$forkliftSelectSql},
                c.business_name,
                c.contact_name,
                c.phone,
                cu.full_name AS customer_user_name,
                cu.phone AS customer_user_phone,
                ua.full_name AS approved_by_name,
                ur.full_name AS rejected_by_name,
                l.name AS location_name,
                l.code AS location_code
            FROM sales_orders o
            JOIN sales_customers c
              ON c.id = o.customer_id
            LEFT JOIN sales_customer_users cu
              ON cu.id = o.customer_user_id
            LEFT JOIN users ua
              ON ua.id = o.approved_by
            LEFT JOIN users ur
              ON ur.id = o.rejected_by
            LEFT JOIN locations l
              ON l.id = o.location_id
            WHERE o.id = ?
              AND o.company_id = ?
        ";
        $params = [$orderId, $companyId];

        if (!$canSeeAllLocations) {
            $sql .= " AND o.location_id = ?";
            $params[] = $currentLocationId;
        }

        $sql .= " LIMIT 1";

        $st = $pdo->prepare($sql);
        $st->execute($params);
        $order = $st->fetch(PDO::FETCH_ASSOC);

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

        $st = $pdo->prepare("
            SELECT
                i.id,
                i.order_id,
                i.product_id,
                i.purchase_order,
                i.oc_relation,
                i.item_customer_id,
                i.item_address_id,
                i.customer_label,
                i.delivery_address AS item_delivery_address,
                i.oc_attachment_missing_reason,
                i.packaging_option_id,
                i.qty,
                COALESCE(i.packages_qty, 0) AS packages_qty,
                COALESCE(i.pallets_qty, 0) AS pallets_qty,
                i.pallet_type,
                i.pallet_specification,
                i.unit_price,
                i.line_total,
                i.notes,
                p.sku,
                p.name AS product_name,
                p.uom,
                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 i
            JOIN products p
              ON p.id = i.product_id
            LEFT JOIN product_packaging_options ppo
              ON ppo.id = i.packaging_option_id
             AND ppo.product_id = i.product_id
             AND ppo.company_id = p.company_id
            WHERE i.order_id = ?
            ORDER BY COALESCE(i.purchase_order,''), p.sku
        ");
        $st->execute([$orderId]);
        $items = $st->fetchAll(PDO::FETCH_ASSOC);

        $st = $pdo->prepare("
            SELECT
                l.id,
                l.old_status,
                l.new_status,
                l.changed_by_user_id,
                l.changed_by_role,
                l.comment,
                l.created_at,
                u.full_name AS changed_by_name
            FROM sales_order_status_log l
            LEFT JOIN users u
              ON u.id = l.changed_by_user_id
            WHERE l.order_id = ?
            ORDER BY l.id DESC
        ");
        $st->execute([$orderId]);
        $logs = $st->fetchAll(PDO::FETCH_ASSOC);

        jexit([
            'ok'    => true,
            'order' => $order,
            'items' => $items,
            'logs'  => $logs,
        ]);
    }

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

        $d = read_json();
        $orderId   = (int)arr_get($d, 'order_id', 0);
        $newStatus = strtoupper(trim((string)arr_get($d, 'new_status', '')));
        $comment   = trim((string)arr_get($d, 'comment', ''));

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

        if (!in_array($newStatus, allowed_sales_statuses(), true)) {
            jexit(['ok' => false, 'error' => 'Estatus inválido'], 400);
        }

        $pdo->beginTransaction();
        try {
            $sql = "
                SELECT *
                FROM sales_orders
                WHERE id = ?
                  AND company_id = ?
            ";
            $params = [$orderId, $companyId];

            if (!$canSeeAllLocations) {
                $sql .= " AND location_id = ?";
                $params[] = $currentLocationId;
            }

            $sql .= " FOR UPDATE";

            $st = $pdo->prepare($sql);
            $st->execute($params);
            $order = $st->fetch(PDO::FETCH_ASSOC);

            if (!$order) {
                throw new RuntimeException('Pedido no encontrado');
            }

            $oldStatus = strtoupper((string)$order['status']);

            if (!can_transition($oldStatus, $newStatus)) {
                throw new RuntimeException("No se puede cambiar de {$oldStatus} a {$newStatus}");
            }

            $approvedBy = null;
            $approvedAt = null;
            $rejectedBy = null;
            $rejectedAt = null;

            if ($newStatus === 'AUTORIZADA') {
                $approvedBy = (int)$GLOBALS['u']['id'];
                $approvedAt = date('Y-m-d H:i:s');
            }

            if ($newStatus === 'RECHAZADA') {
                $rejectedBy = (int)$GLOBALS['u']['id'];
                $rejectedAt = date('Y-m-d H:i:s');
            }

            $st = $pdo->prepare("
                UPDATE sales_orders
                SET
                    status = ?,
                    approved_by = CASE WHEN ? IS NULL THEN approved_by ELSE ? END,
                    approved_at = CASE WHEN ? IS NULL THEN approved_at ELSE ? END,
                    rejected_by = CASE WHEN ? IS NULL THEN rejected_by ELSE ? END,
                    rejected_at = CASE WHEN ? IS NULL THEN rejected_at ELSE ? END,
                    updated_at = NOW()
                WHERE id = ?
            ");
            $st->execute([
                $newStatus,
                $approvedBy, $approvedBy,
                $approvedAt, $approvedAt,
                $rejectedBy, $rejectedBy,
                $rejectedAt, $rejectedAt,
                $orderId,
            ]);

            $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,
                $oldStatus,
                $newStatus,
                (int)$u['id'],
                'VENTAS',
                ($comment === '' ? null : $comment),
            ]);

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




    if ($action === 'botella_routes') {
        $selectedLocationId = (int)($_GET['location_id'] ?? $currentLocationId);
        sales_validate_location($pdo, $companyId, $selectedLocationId);
        $st = $pdo->prepare("
            SELECT route_no, route_code, route_name, vehicle_unit, driver_name, 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, $selectedLocationId]);
        jexit(['ok'=>true, 'rows'=>$st->fetchAll(PDO::FETCH_ASSOC)]);
    }


    if ($action === 'botella_route_stock') {
        $selectedLocationId = (int)($_GET['location_id'] ?? $currentLocationId);
        $routeNo = (int)($_GET['route_no'] ?? 0);
        sales_validate_location($pdo, $companyId, $selectedLocationId);
        if ($routeNo <= 0) {
            jexit(['ok'=>true, 'rows'=>[], 'warehouse_code'=>'']);
        }
        jexit([
            'ok'=>true,
            'rows'=>sales_route_stock_rows($pdo, $companyId, $selectedLocationId, $routeNo),
            'warehouse_code'=>sales_route_warehouse_code($routeNo),
        ]);
    }

    if ($action === 'botella_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 = [];
        }

        /*
         * Fallback compatible:
         * Si todavía no se migró el producto a product_packaging_options,
         * se utiliza la configuración principal existente en products.
         * id = 0 indica que no hay packaging_option_id real, pero permite
         * operar mientras Admin termina de configurar/migrar el empaque.
         */
        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 === 'manual_order_create') {
        if (!$canManage) jexit(['ok' => false, 'error' => 'Sin permiso'], 403);

        $d = read_json();
        $orderType = strtoupper(trim((string)arr_get($d, 'order_type', 'CLIENTE')));
        if (!in_array($orderType, ['CLIENTE','RUTA'], true)) $orderType = 'CLIENTE';
        $orderId = (int)arr_get($d, 'order_id', 0);
        $selectedLocationId = (int)arr_get($d, 'location_id', 0);
        $routeNo = (int)arr_get($d, 'route_no', 0);
        $customerId = (int)arr_get($d, 'customer_id', 0);
        $addressId = (int)arr_get($d, 'address_id', 0);
        $deliveryDate = trim((string)arr_get($d, 'requested_delivery_date', date('Y-m-d')));
        $deliveryTime = trim((string)arr_get($d, 'requested_delivery_time', ''));
        $deliveryAddress = trim((string)arr_get($d, 'delivery_address', ''));
        $purchaseOrder = trim((string)arr_get($d, 'purchase_order', ''));
        $vehicleUnit = trim((string)arr_get($d, 'vehicle_unit', ''));
        $comments = trim((string)arr_get($d, 'comments', ''));
        $itemsRaw = arr_get($d, 'items', []);

        if ($selectedLocationId <= 0) jexit(['ok'=>false,'error'=>'Selecciona la planta / ubicación.'], 400);
        if (!$canSeeAllLocations && $selectedLocationId !== $currentLocationId) jexit(['ok'=>false,'error'=>'No tienes permiso para generar pedidos en esta ubicación.'], 403);
        if ($orderType === 'CLIENTE' && ($customerId <= 0 || !customer_belongs_to_company($pdo, $customerId, $companyId))) {
            jexit(['ok'=>false,'error'=>'Selecciona un cliente registrado de Botella.'], 400);
        }
        if ($orderType === 'CLIENTE' && $routeNo <= 0) {
            $stDefaultRoute = $pdo->prepare("
                SELECT route_no
                FROM route_access_codes
                WHERE company_id=? AND location_id=? AND active=1
                  AND (UPPER(route_code) LIKE '%VENTAS%' OR UPPER(route_name) LIKE '%VENTAS%')
                ORDER BY sort_order, route_no
                LIMIT 1
            ");
            $stDefaultRoute->execute([$companyId, $selectedLocationId]);
            $routeNo = (int)($stDefaultRoute->fetchColumn() ?: 0);
        }
        if ($routeNo <= 0) jexit(['ok'=>false,'error'=>$orderType === 'RUTA' ? 'Selecciona una ruta de reparto.' : 'Configura una ruta activa RUTA-VENTAS para pedidos a cliente.'], 400);
        if ($deliveryDate === '') $deliveryDate = date('Y-m-d');

        if ($orderType === 'RUTA') {
            $addressId = 0;
        } elseif ($addressId > 0) {
            $hasAddrLocation = sales_column_exists($pdo, 'sales_customer_addresses', 'location_id');
            $sqlAddr = "
                SELECT a.id, a.address, a.postal_code
                FROM sales_customer_addresses a
                JOIN sales_customers c ON c.id = a.customer_id
                WHERE a.id = ?
                  AND a.customer_id = ?
                  AND c.company_id = ?
                  AND a.active = 1
            ";
            $paramsAddr = [$addressId, $customerId, $companyId];
            if ($hasAddrLocation) {
                $sqlAddr .= " AND (a.location_id IS NULL OR a.location_id = ?)";
                $paramsAddr[] = $selectedLocationId;
            }
            $sqlAddr .= " LIMIT 1";
            $stAddr = $pdo->prepare($sqlAddr);
            $stAddr->execute($paramsAddr);
            $addrRow = $stAddr->fetch(PDO::FETCH_ASSOC);
            if (!$addrRow) {
                jexit(['ok'=>false,'error'=>'El punto de entrega seleccionado no pertenece al cliente o no está activo para esta localidad.'], 400);
            }
            $deliveryAddress = trim((string)$addrRow['address']);
        } else {
            $addressId = 0;
        }

        if ($orderType === 'CLIENTE' && $deliveryAddress === '') jexit(['ok'=>false,'error'=>'Selecciona o captura el punto de entrega.'], 400);
        if (!is_array($itemsRaw)) $itemsRaw = [];

        $pdo->beginTransaction();
        try {
            sales_validate_location($pdo, $companyId, $selectedLocationId);
            $route = botella_route_row($pdo, $companyId, $selectedLocationId, $routeNo);

            $cleanItems = [];
            foreach ($itemsRaw as $it) {
                $pid = (int)($it['product_id'] ?? 0);
                $packagingId = (int)($it['packaging_option_id'] ?? 0);
                $palletsQtyRaw = (float)($it['pallets_qty'] ?? 0);
                $palletType = trim((string)($it['pallet_type'] ?? ''));
                $palletSpecification = trim((string)($it['pallet_specification'] ?? ''));
                $itemPurchaseOrder = $orderType === 'CLIENTE' ? trim((string)($it['item_purchase_order'] ?? $purchaseOrder)) : '';
                $ocRelation = trim((string)($it['oc_relation'] ?? ''));
                $ocMissingReason = trim((string)($it['oc_attachment_missing_reason'] ?? ''));
                $ocFileIndex = array_key_exists('oc_file_index', $it) && $it['oc_file_index'] !== null ? (int)$it['oc_file_index'] : null;
                $itemCustomerId = $orderType === 'CLIENTE' ? (int)($it['item_customer_id'] ?? $customerId) : 0;
                $itemAddressId = $orderType === 'CLIENTE' ? (int)($it['item_address_id'] ?? $addressId) : 0;
                $itemCustomerLabel = trim((string)($it['customer_label'] ?? ''));
                $itemDeliveryAddress = trim((string)($it['item_delivery_address'] ?? ''));
                if ($orderType === 'CLIENTE' && $itemPurchaseOrder === '') {
                    throw new RuntimeException('Cada renglón OSV debe tener OC del cliente.');
                }
                if ($orderType === 'CLIENTE') {
                    if ($itemCustomerId <= 0 || !customer_belongs_to_company($pdo, $itemCustomerId, $companyId)) {
                        throw new RuntimeException('Cada OC debe tener cliente válido.');
                    }
                    if ($itemAddressId <= 0) {
                        throw new RuntimeException('Cada OC debe tener punto de entrega.');
                    }
                    $hasItemAddrLocation = sales_column_exists($pdo, 'sales_customer_addresses', 'location_id');
                    $sqlItemAddr = "
                        SELECT a.id, a.address, c.business_name, c.customer_code
                        FROM sales_customer_addresses a
                        JOIN sales_customers c ON c.id = a.customer_id
                        WHERE a.id = ? AND a.customer_id = ? AND c.company_id = ? AND a.active = 1
                    ";
                    $paramsItemAddr = [$itemAddressId, $itemCustomerId, $companyId];
                    if ($hasItemAddrLocation) { $sqlItemAddr .= " AND (a.location_id IS NULL OR a.location_id = ?)"; $paramsItemAddr[] = $selectedLocationId; }
                    $sqlItemAddr .= " LIMIT 1";
                    $stItemAddr = $pdo->prepare($sqlItemAddr);
                    $stItemAddr->execute($paramsItemAddr);
                    $itemAddrRow = $stItemAddr->fetch(PDO::FETCH_ASSOC);
                    if (!$itemAddrRow) throw new RuntimeException('El punto de entrega de una OC no pertenece al cliente seleccionado o no está activo.');
                    $itemDeliveryAddress = trim((string)$itemAddrRow['address']);
                    if ($itemCustomerLabel === '') {
                        $cc = trim((string)($itemAddrRow['customer_code'] ?? ''));
                        $bn = trim((string)($itemAddrRow['business_name'] ?? ''));
                        $itemCustomerLabel = trim(($cc !== '' ? $cc . ' · ' : '') . $bn);
                    }
                }

                if ($pid <= 0 || $palletsQtyRaw <= 0) {
                    throw new RuntimeException('Todos los artículos requieren producto, embalaje y cantidad de tarimas.');
                }

                $p = sales_product_lookup($pdo, $companyId, [$pid]);
                if (!isset($p[$pid]) || (int)($p[$pid]['active'] ?? 0) !== 1) {
                    throw new RuntimeException('Uno de los artículos seleccionados no está activo para Botella.');
                }

                $packaging = botella_packaging_row($pdo, $companyId, $pid, $packagingId);
                $calculation = botella_validate_pallets($packaging, $palletsQtyRaw, (string)$p[$pid]['sku']);

                $cleanItems[] = [
                    'product_id' => $pid,
                    'purchase_order' => $itemPurchaseOrder !== '' ? mb_substr($itemPurchaseOrder, 0, 80) : null,
                    'oc_relation' => $ocRelation !== '' ? mb_substr($ocRelation, 0, 60) : null,
                    'oc_attachment_missing_reason' => $ocMissingReason !== '' ? mb_substr($ocMissingReason, 0, 255, 'UTF-8') : null,
                    'oc_file_index' => $ocFileIndex,
                    'item_customer_id' => $itemCustomerId > 0 ? $itemCustomerId : null,
                    'item_address_id' => $itemAddressId > 0 ? $itemAddressId : null,
                    'customer_label' => $itemCustomerLabel !== '' ? mb_substr($itemCustomerLabel, 0, 180, 'UTF-8') : null,
                    'delivery_address' => $itemDeliveryAddress !== '' ? mb_substr($itemDeliveryAddress, 0, 255, 'UTF-8') : null,
                    'packaging_option_id' => $packagingId,
                    'qty' => $calculation['qty_bottles'],
                    'packages_qty' => $calculation['packages_qty'],
                    'pallets_qty' => $calculation['pallets_qty'],
                    'pallet_type' => $palletType !== '' ? mb_substr($palletType, 0, 60) : null,
                    'pallet_specification' => $palletSpecification !== '' ? mb_substr($palletSpecification, 0, 100) : null,
                    'notes' => 'Empaque: ' . ((string)($packaging['packaging_name'] ?? $packaging['packaging_type'])) .
                               ' | ' . $calculation['packages_qty'] . ' paquetes | ' .
                               $calculation['pallets_qty'] . ' tarimas | ' .
                               $calculation['qty_bottles'] . ' botellas'
                ];
            }

            // Botella permite más de una carga en la misma ruta y fecha.
            // Por eso no se bloquea por route_no + deliveryDate.

            if ($orderType === 'RUTA') {
                $routeLabelTmp = trim('Ruta ' . $routeNo . ' · ' . (string)($route['route_code'] ?? '') . ' · ' . (string)($route['route_name'] ?? ''));
                $customerId = sales_find_or_create_customer($pdo, $companyId, $selectedLocationId, $routeLabelTmp, '');
                $deliveryAddress = $routeLabelTmp;
                $customerName = $routeLabelTmp;
            } else {
                $stCustomer = $pdo->prepare("SELECT business_name FROM sales_customers WHERE id=? AND company_id=? AND active=1 LIMIT 1");
                $stCustomer->execute([$customerId, $companyId]);
                $customerName = (string)($stCustomer->fetchColumn() ?: '');
                if ($customerName === '') throw new RuntimeException('El cliente seleccionado está inactivo o no pertenece a Botella.');
            }

            $commentParts = [
                $orderType === 'RUTA' ? 'Orden de salida Botella creada para ruta de reparto.' : 'Orden de salida Botella creada desde Ventas.',
                'Ruta: ' . ((string)($route['route_code'] ?? ('RUTA-' . $routeNo))) . ' · ' . ((string)($route['route_name'] ?? '')),
            ];
            if ($comments !== '') $commentParts[] = $comments;
            $fullComments = implode("\n", $commentParts);
            $isUpdate = $orderId > 0;

            if ($isUpdate) {
                $st = $pdo->prepare("SELECT * FROM sales_orders WHERE id=? AND company_id=? AND location_id=? FOR UPDATE");
                $st->execute([$orderId, $companyId, $selectedLocationId]);
                $existing = $st->fetch(PDO::FETCH_ASSOC);
                if (!$existing) throw new RuntimeException('Pedido no encontrado para editar.');
                if ((int)($existing['outbound_id'] ?? 0) > 0) throw new RuntimeException('No se puede editar un pedido que ya tiene salida generada.');

                $st = $pdo->prepare("
                    UPDATE sales_orders
                    SET customer_id=?, address_id=?, source=?, requested_delivery_date=?, requested_delivery_time=?,
                        route_no=?, vehicle_unit=?, delivery_address=?, purchase_order=?,
                        comments=?, status='AUTORIZADA', total=0,
                        approved_by=?, approved_at=NOW(), updated_at=NOW()
                    WHERE id=? AND company_id=? AND location_id=?
                ");
                $st->execute([
                    $customerId, ($addressId > 0 ? $addressId : null), 'VENTAS', $deliveryDate, ($deliveryTime !== '' ? $deliveryTime : null),
                    $routeNo, ($vehicleUnit !== '' ? $vehicleUnit : ($route['vehicle_unit'] ?? null)),
                    $deliveryAddress, ($purchaseOrder !== '' ? $purchaseOrder : null), $fullComments,
                    (int)$u['id'], $orderId, $companyId, $selectedLocationId
                ]);
                if ($itemsRaw) { $pdo->prepare("DELETE FROM sales_order_items WHERE order_id=?")->execute([$orderId]); }
                $folio = trim((string)($existing['sales_order_ref'] ?? '')) ?: ('OSV-' . str_pad((string)$orderId, 6, '0', STR_PAD_LEFT));
            } else {
                $st = $pdo->prepare("
                    INSERT INTO sales_orders (
                        company_id, location_id, customer_id, address_id, source,
                        requested_delivery_date, requested_delivery_time, route_no,
                        vehicle_unit, delivery_address, purchase_order, comments,
                        status, total, approved_by, approved_at, created_by, created_at, updated_at
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'AUTORIZADA', 0, ?, NOW(), ?, NOW(), NOW())
                ");
                $st->execute([
                    $companyId, $selectedLocationId, $customerId, ($addressId > 0 ? $addressId : null), 'VENTAS', $deliveryDate,
                    ($deliveryTime !== '' ? $deliveryTime : null), $routeNo,
                    ($vehicleUnit !== '' ? $vehicleUnit : ($route['vehicle_unit'] ?? null)),
                    $deliveryAddress, ($purchaseOrder !== '' ? $purchaseOrder : null), $fullComments,
                    (int)$u['id'], (int)$u['id']
                ]);
                $orderId = (int)$pdo->lastInsertId();
                $folio = ($orderType === 'RUTA' ? 'OSA-' : 'OSV-') . str_pad((string)$orderId, 6, '0', STR_PAD_LEFT);
                $pdo->prepare("UPDATE sales_orders SET sales_order_ref=? WHERE id=?")->execute([$folio, $orderId]);
            }

            $stItem = $pdo->prepare("
                INSERT INTO sales_order_items (
                    order_id, product_id, purchase_order, oc_relation, item_customer_id, item_address_id, customer_label, delivery_address, packaging_option_id, qty,
                    packages_qty, pallets_qty, pallet_type, pallet_specification,
                    unit_price, line_total, notes, oc_attachment_missing_reason, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, NOW())
            ");
            $storedOcFileIndexes = [];
            foreach ($cleanItems as $it) {
                $stItem->execute([
                    $orderId, $it['product_id'], $it['purchase_order'], $it['oc_relation'], $it['item_customer_id'], $it['item_address_id'], $it['customer_label'], $it['delivery_address'], $it['packaging_option_id'], $it['qty'],
                    $it['packages_qty'], $it['pallets_qty'], $it['pallet_type'],
                    $it['pallet_specification'], $it['notes'], $it['oc_attachment_missing_reason']
                ]);
                $itemId = (int)$pdo->lastInsertId();
                if ($orderType === 'CLIENTE' && $it['oc_file_index'] !== null) {
                    $ocFileIndex = (int)$it['oc_file_index'];
                    if (!isset($storedOcFileIndexes[$ocFileIndex])) {
                        $fileKey = 'new_order_item_oc_file_' . $ocFileIndex;
                        if (isset($_FILES[$fileKey]) && is_array($_FILES[$fileKey])) {
                            sales_store_order_oc_attachment($pdo, $companyId, $selectedLocationId, $orderId, (string)($it['purchase_order'] ?? ''), $_FILES[$fileKey], (int)$u['id'], $itemId, (string)($it['oc_attachment_missing_reason'] ?? ''));
                        }
                        $storedOcFileIndexes[$ocFileIndex] = true;
                    }
                }
            }
            if ($orderType === 'CLIENTE' && isset($_FILES['new_order_oc_file']) && is_array($_FILES['new_order_oc_file'])) {
                sales_store_order_oc_attachment($pdo, $companyId, $selectedLocationId, $orderId, $purchaseOrder, $_FILES['new_order_oc_file'], (int)$u['id']);
            }

            if (sales_table_exists($pdo, 'sales_order_status_log')) {
                $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 (?, ?, 'AUTORIZADA', ?, 'VENTAS', ?, NOW())
                ");
                $st->execute([$orderId, $isUpdate ? 'AUTORIZADA' : null, (int)$u['id'],
                    $isUpdate ? 'Orden logística Botella actualizada.' : 'Orden logística Botella creada; pendiente de surtir en Almacén.']);
            }

            $pdo->commit();
            jexit([
                'ok'=>true,
                'order_id'=>$orderId,
                'folio'=>$folio,
                'route_no'=>$routeNo,
                'route_label'=>((string)($route['route_code'] ?? '')) . ' · ' . ((string)($route['route_name'] ?? '')),
                'order_type'=>$orderType,
                'message'=>$isUpdate ? 'Orden Botella actualizada correctamente.' : ($orderType === 'RUTA' ? 'Orden de ruta de reparto creada correctamente. Almacén continuará con la carga.' : 'Orden de salida a cliente creada correctamente. Almacén continuará con la carga.'),
                'warnings'=>$GLOBALS['sales_upload_warnings'] ?? []
            ]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            $message = $e->getMessage();
            $code = str_starts_with($message, '[PALLET_DECIMAL]') ? 'PALLET_DECIMAL' : 'VALIDATION';
            $message = str_replace('[PALLET_DECIMAL] ', '', $message);
            jexit(['ok'=>false, 'validation_code'=>$code, 'error'=>$message], 400);
        }
    }

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

        $d = read_json();
        $orderType = strtoupper(trim((string)arr_get($d, 'order_type', 'CLIENTE')));
        if (!in_array($orderType, ['CLIENTE','RUTA'], true)) $orderType = 'CLIENTE';
        $orderId = (int)arr_get($d, 'order_id', 0);
        $workDate = trim((string)arr_get($d, 'work_date', date('Y-m-d')));
        $notes = trim((string)arr_get($d, 'notes', ''));

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

        $pdo->beginTransaction();
        try {
            $sql = "
                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 = ?
            ";
            $params = [$orderId, $companyId];

            if (!$canSeeAllLocations) {
                $sql .= " AND o.location_id = ?";
                $params[] = $currentLocationId;
            }
            $sql .= " FOR UPDATE";

            $st = $pdo->prepare($sql);
            $st->execute($params);
            $order = $st->fetch(PDO::FETCH_ASSOC);

            if (!$order) {
                throw new RuntimeException('Pedido no encontrado.');
            }

            $oldStatus = strtoupper((string)$order['status']);
            $folio = trim((string)($order['sales_order_ref'] ?? ''));
            if ($folio === '') {
                $folio = 'PED-' . (int)$order['id'];
            }

            if (!empty($order['outbound_id'])) {
                $existingOutboundId = (int)$order['outbound_id'];
                $pdo->commit();
                jexit([
                    'ok' => true,
                    'already_processed' => true,
                    'message' => 'El pedido ya tiene salida generada.',
                    'order_id' => $orderId,
                    'outbound_id' => $existingOutboundId,
                    'pdf_url' => sales_pdf_url($orderId, $existingOutboundId, $folio),
                    'warnings' => [],
                ]);
            }

            if ($oldStatus !== 'AUTORIZADA') {
                throw new RuntimeException('Solo se pueden generar salidas de pedidos AUTORIZADA. Estatus actual: ' . $oldStatus);
            }

            $orderLocationId = (int)$order['location_id'];
            $channelId = sales_default_channel_id($pdo, $companyId);
            if ($channelId <= 0) {
                throw new RuntimeException('No existe un canal de salida activo para la empresa. Configura outbound_channels.');
            }

            $st = $pdo->prepare("
                SELECT
                    i.product_id,
                    SUM(i.qty) AS qty,
                    MAX(p.sku) AS sku,
                    MAX(p.name) AS product_name,
                    MAX(p.uom) AS uom
                FROM sales_order_items i
                JOIN products p ON p.id = i.product_id
                WHERE i.order_id = ?
                GROUP BY i.product_id
                ORDER BY MAX(p.sku), MAX(p.name)
            ");
            $st->execute([$orderId]);
            $items = $st->fetchAll(PDO::FETCH_ASSOC);

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

            $dispatchItems = [];
            $warnings = [];
            foreach ($items as $it) {
                $pid = (int)$it['product_id'];
                $requestedRaw = (float)$it['qty'];

                if ($requestedRaw <= 0) {
                    continue;
                }
                if (!sales_is_whole_units($requestedRaw)) {
                    throw new RuntimeException('El pedido contiene una cantidad decimal en ' . (string)$it['sku'] . '. Edita el pedido y captura piezas enteras antes de generar la salida.');
                }

                $requestedQty = (float)(int)round($requestedRaw);
                $stock = (float)sales_available_whole_units($pdo, $companyId, $orderLocationId, $pid, (string)$it['sku']);

                if ($stock <= 0) {
                    $warnings[] = 'Sin existencia para ' . (string)$it['sku'] . ' - ' . (string)$it['product_name'] . '. No se descontó.';
                    continue;
                }

                $qtyToDispatch = min($requestedQty, $stock);
                if ($qtyToDispatch < $requestedQty) {
                    $warnings[] = 'Existencia parcial para ' . (string)$it['sku'] . ' - ' . (string)$it['product_name'] . '. Solicitado: ' . number_format($requestedQty, 0, '.', '') . ', descontado: ' . number_format($qtyToDispatch, 0, '.', '') . '.';
                }

                $dispatchItems[] = [
                    'product_id' => $pid,
                    'qty' => $qtyToDispatch,
                    'sku' => (string)$it['sku'],
                    'product_name' => (string)$it['product_name'],
                ];
            }

            if (!$dispatchItems) {
                throw new RuntimeException('No hay artículos con existencia disponible para generar la salida.');
            }

            $notesExtra = [];
            $notesExtra[] = 'Salida generada desde módulo Ventas';
            $notesExtra[] = 'Pedido: ' . $folio;
            if (!empty($order['business_name'])) $notesExtra[] = 'Cliente: ' . (string)$order['business_name'];
            if ($notes !== '') $notesExtra[] = 'Notas ventas: ' . $notes;
            if ($warnings) $notesExtra[] = 'Advertencias: ' . implode(' / ', $warnings);
            $fullNotes = implode(' | ', $notesExtra);

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

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

            $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 (
                    ?, ?, ?, ?, ?, ?, 'OUT',
                    ?, 'OUTBOUND', ?, ?, ?, NOW()
                )
            ");

            $movementSummary = [];
            foreach ($dispatchItems as $it) {
                $productIdMove = (int)$it['product_id'];
                $qtyMove = (float)$it['qty'];
                $stItem->execute([$outboundId, $productIdMove, $qtyMove]);

                foreach (sales_allocate_inventory_out($pdo, $companyId, $orderLocationId, $productIdMove, (string)$it['sku'], $qtyMove) as $alloc) {
                    $moveNotes = $fullNotes . ' | WH: ' . (string)$alloc['warehouse_code'];
                    $stMove->execute([
                        $companyId,
                        $orderLocationId,
                        (int)$alloc['warehouse_id'],
                        $workDate,
                        $productIdMove,
                        (float)$alloc['qty'],
                        $channelId,
                        $outboundId,
                        $moveNotes,
                        (int)$u['id'],
                    ]);
                    $movementSummary[] = [
                        'product_id' => $productIdMove,
                        'sku' => (string)$it['sku'],
                        'product_name' => (string)$it['product_name'],
                        'warehouse_id' => (int)$alloc['warehouse_id'],
                        'warehouse_code' => (string)$alloc['warehouse_code'],
                        'qty' => (float)$alloc['qty'],
                        'move_type' => 'OUT',
                        'ref_type' => 'OUTBOUND',
                    ];
                }
            }

            $updateCols = [
                'outbound_id = ?',
                "status = 'SURTIENDO'",
                'updated_at = NOW()',
            ];
            $updateParams = [$outboundId];
            if (sales_column_exists($pdo, 'sales_orders', 'fulfilled_at')) {
                $updateCols[] = 'fulfilled_at = NOW()';
            }
            $updateParams[] = $orderId;
            $st = $pdo->prepare('UPDATE sales_orders SET ' . implode(', ', $updateCols) . ' WHERE id = ?');
            $st->execute($updateParams);

            if (sales_table_exists($pdo, 'sales_order_status_log')) {
                $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,
                    $oldStatus,
                    'SURTIENDO',
                    (int)$u['id'],
                    'VENTAS',
                    'Salida generada desde Ventas. Outbound #' . $outboundId,
                ]);
            }

            $pdo->commit();
            jexit([
                'ok' => true,
                'order_id' => $orderId,
                'outbound_id' => $outboundId,
                'pdf_url' => sales_pdf_url($orderId, $outboundId, $folio),
                'warnings' => $warnings,
                'dispatched_items' => $dispatchItems,
                'movements' => $movementSummary ?? [],
                'message' => 'Salida generada correctamente.',
            ]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            jexit(['ok' => false, 'error' => $e->getMessage()], 500);
        }
    }

    if ($action === 'inventory_locations' || $action === 'location_options') {
        $st = $pdo->prepare("
            SELECT id, name, code
            FROM locations
            WHERE company_id = ?
              AND active = 1
            ORDER BY name
        ");
        $st->execute([$companyId]);

        jexit([
            'ok' => true,
            'rows' => $st->fetchAll(PDO::FETCH_ASSOC)
        ]);
    }

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

        $st = $pdo->prepare("
            SELECT id, name, code
            FROM locations
            WHERE id = ?
              AND company_id = ?
              AND active = 1
            LIMIT 1
        ");
        $st->execute([$selectedLocationId, $companyId]);
        $loc = $st->fetch(PDO::FETCH_ASSOC);

        if (!$loc) {
            jexit(['ok' => false, 'error' => 'Ubicación no encontrada'], 404);
        }

        $snap = build_inventory_snapshot($pdo, $companyId, $selectedLocationId);
        $rows = $snap['rows'];

        usort($rows, function(array $a, array $b): int {
            return strcmp((string)$a['sku'], (string)$b['sku']);
        });

        jexit([
            'ok'       => true,
            'location' => $loc,
            'rows'     => $rows,
            'inventory_note' => $snap['inventory_note'] ?? sales_inventory_note($pdo, $companyId, $selectedLocationId),
        ]);
    }

    if ($action === 'customers') {
        if (!$canManage) jexit(['ok' => false, 'error' => 'Sin permiso'], 403);

        $q = trim((string)($_GET['q'] ?? ''));
        $selectedLocationId = (int)($_GET['location_id'] ?? $currentLocationId);

        $hasCustomerLocation = sales_column_exists($pdo, 'sales_customers', 'location_id');
        $where = ["c.company_id = ?"];
        $params = [$companyId];

        if ($hasCustomerLocation) {
            $loc = $selectedLocationId > 0 ? $selectedLocationId : $currentLocationId;
            if ($loc > 0) {
                $where[] = "(c.location_id IS NULL OR c.location_id = ?)";
                $params[] = $loc;
            }
        }

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

        $locSelect = $hasCustomerLocation ? 'c.location_id,' : 'NULL AS location_id,';
        $sql = "
            SELECT
                c.id,
                {$locSelect}
                c.customer_code,
                c.business_name,
                c.contact_name,
                c.phone,
                c.email,
                c.address,
                c.postal_code,
                c.active,
                c.created_at,
                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.active DESC, c.business_name ASC
        ";
        $st = $pdo->prepare($sql);
        $st->execute($params);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'customer_save') {
        if (!$canManage) jexit(['ok' => false, 'error' => 'Sin permiso'], 403);

        $d = read_json();
        $customerId = (int)arr_get($d, 'customer_id', 0);
        $locationId = (int)arr_get($d, 'location_id', $currentLocationId);
        $customerCode = trim((string)arr_get($d, 'customer_code', ''));
        $businessName = trim((string)arr_get($d, 'business_name', ''));
        $contactName = trim((string)arr_get($d, 'contact_name', ''));
        $phone = normalize_phone((string)arr_get($d, 'phone', ''));
        $email = trim((string)arr_get($d, 'email', ''));
        $address = trim((string)arr_get($d, 'address', ''));
        $postalCode = trim((string)arr_get($d, 'postal_code', ''));
        $active = (int)arr_get($d, 'active', 1);

        if ($customerCode === '') jexit(['ok' => false, 'error' => 'Captura el número de cliente del sistema principal.'], 400);
        if ($businessName === '') jexit(['ok' => false, 'error' => 'Captura el nombre del cliente.'], 400);
        if ($contactName === '') jexit(['ok' => false, 'error' => 'Captura la razón social.'], 400);
        if ($address === '') jexit(['ok' => false, 'error' => 'Captura el punto de entrega principal.'], 400);

        if ($locationId <= 0) $locationId = $currentLocationId;
        sales_validate_location($pdo, $companyId, $locationId);
        if (!$canSeeAllLocations && $locationId !== $currentLocationId) {
            jexit(['ok' => false, 'error' => 'No tienes permiso para clientes de otra localidad.'], 403);
        }
        if (!in_array($active, [0,1], true)) $active = 1;

        $stDup = $pdo->prepare("
            SELECT id
            FROM sales_customers
            WHERE company_id = ?
              AND customer_code = ?
              AND id <> ?
            LIMIT 1
        ");
        $stDup->execute([$companyId, $customerCode, $customerId]);
        if ((int)($stDup->fetchColumn() ?: 0) > 0) {
            jexit(['ok' => false, 'error' => 'Ya existe otro cliente con ese número de cliente.'], 409);
        }

        $pdo->beginTransaction();
        try {
            if ($customerId > 0) {
                $sql = "UPDATE sales_customers
                        SET customer_code=?, business_name=?, contact_name=?, phone=?, email=?, address=?, postal_code=?, active=?, updated_at=NOW()";
                $params = [
                    $customerCode,
                    $businessName,
                    $contactName,
                    ($phone !== '' ? $phone : null),
                    ($email !== '' ? $email : null),
                    $address,
                    ($postalCode !== '' ? $postalCode : null),
                    $active
                ];

                if (sales_column_exists($pdo, 'sales_customers', 'location_id')) {
                    $sql .= ", location_id=?";
                    $params[] = $locationId;
                }

                $sql .= " WHERE id=? AND company_id=?";
                $params[] = $customerId;
                $params[] = $companyId;

                if (!$canSeeAllLocations && sales_column_exists($pdo, 'sales_customers', 'location_id')) {
                    $sql .= " AND (location_id IS NULL OR location_id=?)";
                    $params[] = $currentLocationId;
                }

                $st = $pdo->prepare($sql);
                $st->execute($params);
                if ($st->rowCount() <= 0) {
                    throw new RuntimeException('No se encontró el cliente para actualizar o no tienes permiso.');
                }
            } else {
                if (sales_column_exists($pdo, 'sales_customers', 'location_id')) {
                    $pdo->prepare("
                        INSERT INTO sales_customers (
                            company_id, location_id, customer_code, business_name, contact_name,
                            phone, email, address, postal_code, active, created_at, updated_at
                        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
                    ")->execute([
                        $companyId,
                        $locationId,
                        $customerCode,
                        $businessName,
                        $contactName,
                        ($phone !== '' ? $phone : null),
                        ($email !== '' ? $email : null),
                        $address,
                        ($postalCode !== '' ? $postalCode : null),
                        $active
                    ]);
                } else {
                    $pdo->prepare("
                        INSERT INTO sales_customers (
                            company_id, customer_code, business_name, contact_name,
                            phone, email, address, postal_code, active, created_at, updated_at
                        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
                    ")->execute([
                        $companyId,
                        $customerCode,
                        $businessName,
                        $contactName,
                        ($phone !== '' ? $phone : null),
                        ($email !== '' ? $email : null),
                        $address,
                        ($postalCode !== '' ? $postalCode : null),
                        $active
                    ]);
                }
                $customerId = (int)$pdo->lastInsertId();
            }

            /*
             * Mantiene el punto de entrega principal también en la tabla de
             * direcciones, para que el vendedor lo pueda seleccionar en nuevas
             * órdenes y pueda agregar más destinos del mismo cliente.
             */
            if (sales_table_exists($pdo, 'sales_customer_addresses')) {
                $hasAddrLocation = sales_column_exists($pdo, 'sales_customer_addresses', 'location_id');
                $stAddr = $pdo->prepare("
                    SELECT id
                    FROM sales_customer_addresses
                    WHERE customer_id = ?
                      AND is_default = 1
                    ORDER BY id ASC
                    LIMIT 1
                ");
                $stAddr->execute([$customerId]);
                $defaultAddressId = (int)($stAddr->fetchColumn() ?: 0);

                if ($defaultAddressId > 0) {
                    $sql = "UPDATE sales_customer_addresses
                            SET label=?, address=?, postal_code=?, is_default=1, active=?
                            " . ($hasAddrLocation ? ", location_id=?" : "") . "
                            WHERE id=? AND customer_id=?";
                    $params = [
                        'Punto de entrega principal',
                        $address,
                        ($postalCode !== '' ? $postalCode : null),
                        $active
                    ];
                    if ($hasAddrLocation) $params[] = $locationId;
                    $params[] = $defaultAddressId;
                    $params[] = $customerId;
                    $pdo->prepare($sql)->execute($params);
                } else {
                    $cols = "customer_id, label, address, postal_code, is_default, active, created_at";
                    $vals = "?, ?, ?, ?, 1, ?, NOW()";
                    $params = [
                        $customerId,
                        'Punto de entrega principal',
                        $address,
                        ($postalCode !== '' ? $postalCode : null),
                        $active
                    ];
                    if ($hasAddrLocation) {
                        $cols = "customer_id, location_id, label, address, postal_code, is_default, active, created_at";
                        $vals = "?, ?, ?, ?, ?, 1, ?, NOW()";
                        $params = [
                            $customerId,
                            $locationId,
                            'Punto de entrega principal',
                            $address,
                            ($postalCode !== '' ? $postalCode : null),
                            $active
                        ];
                    }
                    $pdo->prepare("INSERT INTO sales_customer_addresses ({$cols}) VALUES ({$vals})")->execute($params);
                }
            }

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

    if ($action === 'customer_addresses') {
        if (!$canManage) jexit(['ok' => false, 'error' => 'Sin permiso'], 403);

        $customerId = (int)($_GET['customer_id'] ?? 0);
        if ($customerId <= 0 || !customer_belongs_to_company($pdo, $customerId, $companyId)) {
            jexit(['ok' => false, 'error' => 'Cliente inválido'], 400);
        }

        $hasAddrLocation = sales_column_exists($pdo, 'sales_customer_addresses', 'location_id');
        $locSelect = $hasAddrLocation ? 'a.location_id,' : 'NULL AS location_id,';

        $st = $pdo->prepare("
            SELECT
                a.id,
                a.customer_id,
                {$locSelect}
                a.label,
                a.address,
                a.postal_code,
                a.is_default,
                a.active,
                a.created_at,
                l.name AS location_name,
                l.code AS location_code
            FROM sales_customer_addresses a
            LEFT JOIN sales_customers c ON c.id = a.customer_id
            LEFT JOIN locations l ON l.id = a.location_id AND l.company_id = c.company_id
            WHERE a.customer_id = ?
            ORDER BY a.is_default DESC, a.active DESC, a.id DESC
        ");
        $st->execute([$customerId]);
        jexit(['ok' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'customer_address_save') {
        if (!$canManage) jexit(['ok' => false, 'error' => 'Sin permiso'], 403);

        $d = read_json();
        $addressId = (int)arr_get($d, 'address_id', 0);
        $customerId = (int)arr_get($d, 'customer_id', 0);
        $locationId = (int)arr_get($d, 'location_id', $currentLocationId);
        $label = trim((string)arr_get($d, 'label', ''));
        $address = trim((string)arr_get($d, 'address', ''));
        $postalCode = trim((string)arr_get($d, 'postal_code', ''));
        $isDefault = (int)arr_get($d, 'is_default', 0) === 1 ? 1 : 0;
        $active = (int)arr_get($d, 'active', 1) === 0 ? 0 : 1;

        if ($customerId <= 0 || !customer_belongs_to_company($pdo, $customerId, $companyId)) {
            jexit(['ok' => false, 'error' => 'Cliente inválido'], 400);
        }
        if ($locationId <= 0) $locationId = $currentLocationId;
        sales_validate_location($pdo, $companyId, $locationId);
        if (!$canSeeAllLocations && $locationId !== $currentLocationId) {
            jexit(['ok' => false, 'error' => 'No tienes permiso para direcciones de otra localidad.'], 403);
        }
        if ($label === '') jexit(['ok' => false, 'error' => 'Captura el nombre del punto de entrega.'], 400);
        if ($address === '') jexit(['ok' => false, 'error' => 'Captura la dirección de entrega.'], 400);

        $hasAddrLocation = sales_column_exists($pdo, 'sales_customer_addresses', 'location_id');

        if ($isDefault) {
            $pdo->prepare("UPDATE sales_customer_addresses SET is_default = 0 WHERE customer_id = ?")->execute([$customerId]);
        }

        if ($addressId > 0) {
            $sql = "UPDATE sales_customer_addresses
                    SET label=?, address=?, postal_code=?, is_default=?, active=?
                    " . ($hasAddrLocation ? ", location_id=?" : "") . "
                    WHERE id=? AND customer_id=?";
            $params = [
                $label,
                $address,
                ($postalCode !== '' ? $postalCode : null),
                $isDefault,
                $active
            ];
            if ($hasAddrLocation) $params[] = $locationId;
            $params[] = $addressId;
            $params[] = $customerId;
            $pdo->prepare($sql)->execute($params);
        } else {
            if ($hasAddrLocation) {
                $pdo->prepare("
                    INSERT INTO sales_customer_addresses (
                        customer_id, location_id, label, address, postal_code, is_default, active, created_at
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
                ")->execute([
                    $customerId,
                    $locationId,
                    $label,
                    $address,
                    ($postalCode !== '' ? $postalCode : null),
                    $isDefault,
                    $active
                ]);
            } else {
                $pdo->prepare("
                    INSERT INTO sales_customer_addresses (
                        customer_id, label, address, postal_code, is_default, active, created_at
                    ) VALUES (?, ?, ?, ?, ?, ?, NOW())
                ")->execute([
                    $customerId,
                    $label,
                    $address,
                    ($postalCode !== '' ? $postalCode : null),
                    $isDefault,
                    $active
                ]);
            }
            $addressId = (int)$pdo->lastInsertId();
        }

        jexit(['ok' => true, 'address_id' => $addressId]);
    }

    if ($action === 'customer_address_toggle') {
        if (!$canManage) jexit(['ok' => false, 'error' => 'Sin permiso'], 403);
        $d = read_json();
        $addressId = (int)arr_get($d, 'address_id', 0);
        $active = (int)arr_get($d, 'active', -1);
        if ($addressId <= 0 || !in_array($active, [0,1], true)) {
            jexit(['ok' => false, 'error' => 'Datos inválidos'], 400);
        }
        $pdo->prepare("
            UPDATE sales_customer_addresses a
            JOIN sales_customers c ON c.id = a.customer_id
            SET a.active = ?
            WHERE a.id = ?
              AND c.company_id = ?
        ")->execute([$active, $addressId, $companyId]);
        jexit(['ok' => true]);
    }

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

        $d = read_json();
        $customerId = (int)arr_get($d, 'customer_id', 0);
        $active     = (int)arr_get($d, 'active', -1);

        if ($customerId <= 0 || !in_array($active, [0, 1], true)) {
            jexit(['ok' => false, 'error' => 'Datos inválidos'], 400);
        }

        $st = $pdo->prepare("
            UPDATE sales_customers
            SET active = ?, updated_at = NOW()
            WHERE id = ? AND company_id = ?
        ");
        $st->execute([$active, $customerId, $companyId]);

        jexit(['ok' => true]);
    }

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

        $q = trim((string)($_GET['q'] ?? ''));

        $sql = "
            SELECT id, sku, name, uom, active
            FROM products
            WHERE company_id = ?
        ";
        $params = [$companyId];

        if ($q !== '') {
            $sql .= " AND (sku LIKE ? OR name LIKE ?)";
            $params[] = '%' . $q . '%';
            $params[] = '%' . $q . '%';
        }

        $sql .= " ORDER BY active DESC, sku ASC";

        $st = $pdo->prepare($sql);
        $st->execute($params);

        jexit([
            'ok' => true,
            'rows' => $st->fetchAll(PDO::FETCH_ASSOC),
        ]);
    }

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

        $d = read_json();
        $productId = (int)arr_get($d, 'product_id', 0);
        $active    = (int)arr_get($d, 'active', -1);

        if ($productId <= 0 || !in_array($active, [0, 1], true)) {
            jexit(['ok' => false, 'error' => 'Datos inválidos'], 400);
        }

        $st = $pdo->prepare("
            UPDATE products
            SET active = ?
            WHERE id = ? AND company_id = ?
        ");
        $st->execute([$active, $productId, $companyId]);

        jexit(['ok' => true]);
    }


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

        $customerId = (int)($_GET['customer_id'] ?? 0);
        if ($customerId <= 0 || !customer_belongs_to_company($pdo, $customerId, $companyId)) {
            jexit(['ok' => false, 'error' => 'Cliente inválido'], 400);
        }

        $st = $pdo->prepare("
            SELECT cu.id, cu.customer_id, cu.full_name, cu.phone, cu.username, cu.role, cu.email, cu.active, cu.last_login_at, cu.created_at, cu.updated_at,
                   sc.business_name
            FROM sales_customer_users cu
            JOIN sales_customers sc ON sc.id = cu.customer_id
            WHERE cu.customer_id = ?
            ORDER BY cu.active DESC, cu.full_name ASC, cu.id DESC
        ");
        $st->execute([$customerId]);

        jexit([
            'ok' => true,
            'rows' => $st->fetchAll(PDO::FETCH_ASSOC),
        ]);
    }

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

        $d = read_json();
        $customerId = (int)arr_get($d, 'customer_id', 0);
        $fullName   = trim((string)arr_get($d, 'full_name', ''));
        $phone      = normalize_phone((string)arr_get($d, 'phone', ''));
        $password   = (string)arr_get($d, 'password', '');
        $email      = trim((string)arr_get($d, 'email', ''));
        $role       = trim((string)arr_get($d, 'role', 'cliente'));
        $active     = (int)arr_get($d, 'active', 1);

        if ($customerId <= 0 || !customer_belongs_to_company($pdo, $customerId, $companyId)) {
            jexit(['ok' => false, 'error' => 'Cliente inválido'], 400);
        }
        if ($fullName === '') {
            jexit(['ok' => false, 'error' => 'Captura el nombre del usuario'], 400);
        }
        if ($phone === '' || mb_strlen($phone) < 8) {
            jexit(['ok' => false, 'error' => 'Captura un teléfono válido'], 400);
        }
        if ($password === '' || mb_strlen($password) < 4) {
            jexit(['ok' => false, 'error' => 'La contraseña debe tener al menos 4 caracteres'], 400);
        }
        if (!in_array($role, ['cliente', 'tienda'], true)) {
            $role = 'cliente';
        }
        if (!in_array($active, [0, 1], true)) {
            $active = 1;
        }

        $st = $pdo->prepare("SELECT id FROM sales_customer_users WHERE phone = ? LIMIT 1");
        $st->execute([$phone]);
        if ($st->fetchColumn()) {
            jexit(['ok' => false, 'error' => 'Ya existe un usuario con ese teléfono'], 409);
        }

        $passHash = password_hash($password, PASSWORD_DEFAULT);
        $username = $phone;
        $emailVal = ($email === '' ? null : $email);

        $st = $pdo->prepare("
            INSERT INTO sales_customer_users (
                customer_id, role, username, full_name, phone, email, pass_hash, active, created_at, updated_at
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
        ");
        $st->execute([$customerId, $role, $username, $fullName, $phone, $emailVal, $passHash, $active]);

        jexit(['ok' => true]);
    }

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

        $d = read_json();
        $userId   = (int)arr_get($d, 'user_id', 0);
        $fullName = trim((string)arr_get($d, 'full_name', ''));
        $phone    = normalize_phone((string)arr_get($d, 'phone', ''));
        $email    = trim((string)arr_get($d, 'email', ''));
        $role     = trim((string)arr_get($d, 'role', 'cliente'));
        $active   = (int)arr_get($d, 'active', 1);

        if ($userId <= 0) {
            jexit(['ok' => false, 'error' => 'Usuario inválido'], 400);
        }
        if ($fullName === '') {
            jexit(['ok' => false, 'error' => 'Captura el nombre del usuario'], 400);
        }
        if ($phone === '' || mb_strlen($phone) < 8) {
            jexit(['ok' => false, 'error' => 'Captura un teléfono válido'], 400);
        }
        if (!in_array($role, ['cliente', 'tienda'], true)) {
            $role = 'cliente';
        }
        if (!in_array($active, [0, 1], true)) {
            $active = 1;
        }

        $st = $pdo->prepare("
            SELECT cu.id
            FROM sales_customer_users cu
            JOIN sales_customers sc ON sc.id = cu.customer_id
            WHERE cu.id = ? AND sc.company_id = ?
            LIMIT 1
        ");
        $st->execute([$userId, $companyId]);
        if (!$st->fetchColumn()) {
            jexit(['ok' => false, 'error' => 'Usuario no encontrado'], 404);
        }

        $st = $pdo->prepare("SELECT id FROM sales_customer_users WHERE phone = ? AND id <> ? LIMIT 1");
        $st->execute([$phone, $userId]);
        if ($st->fetchColumn()) {
            jexit(['ok' => false, 'error' => 'Ya existe otro usuario con ese teléfono'], 409);
        }

        $username = $phone;
        $emailVal = ($email === '' ? null : $email);

        $st = $pdo->prepare("
            UPDATE sales_customer_users
            SET full_name = ?, phone = ?, username = ?, email = ?, role = ?, active = ?, updated_at = NOW()
            WHERE id = ?
        ");
        $st->execute([$fullName, $phone, $username, $emailVal, $role, $active, $userId]);

        jexit(['ok' => true]);
    }

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

        $d = read_json();
        $userId   = (int)arr_get($d, 'user_id', 0);
        $password = (string)arr_get($d, 'password', '');

        if ($userId <= 0) {
            jexit(['ok' => false, 'error' => 'Usuario inválido'], 400);
        }
        if ($password === '' || mb_strlen($password) < 4) {
            jexit(['ok' => false, 'error' => 'La contraseña debe tener al menos 4 caracteres'], 400);
        }

        $st = $pdo->prepare("
            SELECT cu.id
            FROM sales_customer_users cu
            JOIN sales_customers sc ON sc.id = cu.customer_id
            WHERE cu.id = ? AND sc.company_id = ?
            LIMIT 1
        ");
        $st->execute([$userId, $companyId]);
        if (!$st->fetchColumn()) {
            jexit(['ok' => false, 'error' => 'Usuario no encontrado'], 404);
        }

        $passHash = password_hash($password, PASSWORD_DEFAULT);
        $st = $pdo->prepare("UPDATE sales_customer_users SET pass_hash = ?, updated_at = NOW() WHERE id = ?");
        $st->execute([$passHash, $userId]);

        jexit(['ok' => true]);
    }

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

        $d = read_json();
        $userId = (int)arr_get($d, 'user_id', 0);
        $active = (int)arr_get($d, 'active', -1);

        if ($userId <= 0 || !in_array($active, [0, 1], true)) {
            jexit(['ok' => false, 'error' => 'Datos inválidos'], 400);
        }

        $st = $pdo->prepare("
            UPDATE sales_customer_users cu
            JOIN sales_customers sc ON sc.id = cu.customer_id
            SET cu.active = ?, cu.updated_at = NOW()
            WHERE cu.id = ? AND sc.company_id = ?
        ");
        $st->execute([$active, $userId, $companyId]);

        jexit(['ok' => true]);
    }

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

/* ============================================================
   UI
============================================================ */
$currentSalesContext = sales_location_context($pdo, $companyId, $currentLocationId);
$isHieloBarraScope = !empty($currentSalesContext['is_hielo_scope']);
$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 · Botella</title>

  <link rel="preconnect" href="https://fonts.googleapis.com">
  <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:#031126;
      --bg2:#071d3b;
      --line:rgba(145,188,255,.14);
      --txt:#eef4ff;
      --txt-strong:#ffffff;
      --txt-soft:#c7d8f4;
      --gold:#f4be59;
      --green:#72f0a5;
      --danger:#ff8fa3;
      --info:#57a5ff;
      --shadow:0 20px 60px rgba(0,0,0,.35);
      --radius:24px;
    }

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

    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,.20), transparent 26%),
        radial-gradient(circle at 88% 80%, rgba(0,190,255,.10), transparent 22%),
        linear-gradient(135deg, var(--bg1) 0%, #03152e 44%, var(--bg2) 100%);
      background-attachment:fixed;
    }

    .topbar{
      background:rgba(5,18,43,.76);
      backdrop-filter:blur(12px);
      border-bottom:1px solid rgba(255,255,255,.08);
      box-shadow:0 8px 24px rgba(0,0,0,.18);
      position:sticky;
      top:0;
      z-index:1040;
    }

    .page-wrap{padding:28px 18px 40px}
    .shell{max-width:1500px;margin:0 auto}
    .panel-pad{padding:22px}
    .brand-title{font-weight:800;font-size:1.9rem;color:var(--txt-strong)}
    .brand-sub,.muted,.small,.small-note,.form-label{color:var(--txt-soft)!important}

    .glass{
      background:linear-gradient(180deg, rgba(8,26,61,.94), rgba(4,18,44,.92));
      border:1px solid rgba(145,188,255,.14);
      border-radius:var(--radius);
      box-shadow:var(--shadow);
      backdrop-filter:blur(12px);
    }

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

    .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;
    }

    .toolbar-btn:hover{transform:translateY(-1px);color:#fff!important;background:rgba(28,53,104,.92)}
    .btn-primary-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-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-danger-soft{
      color:#fff!important;font-weight:700;border:none;border-radius:18px;
      background:linear-gradient(135deg, #c44b5f, #ef7388);
      box-shadow:0 12px 32px rgba(196,75,95,.24)
    }
    .btn-warning-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)
    }

    .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-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:.95rem;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;
    }

    .form-control,.form-select{
      min-height:48px;
      border-radius:16px;
      color:#eef4ff!important;
      caret-color:#ffffff;
      background:rgba(8,24,58,.84)!important;
      border:1px solid rgba(160,197,255,.18);
      box-shadow:none!important;
      transition:border-color .18s ease, box-shadow .18s ease, background-color .18s ease;
    }
    .form-control::placeholder{color:#9fb4d8;opacity:1}
    .form-control:focus,.form-select:focus,
    .form-control:active,.form-select:active{
      color:#ffffff!important;
      background:rgba(10,30,70,.96)!important;
      border-color:rgba(118,176,255,.58)!important;
      box-shadow:0 0 0 .22rem rgba(64,132,255,.18)!important;
      outline:none;
    }
    .form-control:disabled,.form-select:disabled,
    .form-control[readonly]{
      color:#d3e2fb!important;
      background:rgba(18,34,66,.88)!important;
      border-color:rgba(160,197,255,.12)!important;
      opacity:1;
    }
    .form-select option,
    .form-select optgroup{
      color:#eef4ff;
      background:#0b1f45;
    }
    .form-control::-webkit-date-and-time-value{color:#eef4ff}
    .form-control::-webkit-calendar-picker-indicator{
      filter:invert(1) brightness(1.2);
      opacity:.9;
      cursor:pointer;
    }
    .form-control::selection,
    .form-select::selection{
      color:#ffffff;
      background:rgba(76,146,255,.45);
    }
    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:#eef4ff!important;
      -webkit-box-shadow:0 0 0 1000px rgba(10,30,70,.96) inset!important;
      box-shadow:0 0 0 1000px rgba(10,30,70,.96) inset!important;
      transition:background-color 9999s ease-out 0s;
      caret-color:#ffffff;
    }

    .badge-status{
      display:inline-flex;align-items:center;padding:8px 12px;border-radius:999px;
      font-weight:800;font-size:.82rem;letter-spacing:.2px
    }

    .st-pendiente{background:rgba(255,255,255,.10);color:#eef4ff;border:1px solid rgba(255,255,255,.12)}
    .st-revision{background:rgba(87,165,255,.16);color:#8fc5ff;border:1px solid rgba(87,165,255,.24)}
    .st-autorizada{background:rgba(114,240,165,.16);color:#7cf0ae;border:1px solid rgba(114,240,165,.24)}
    .st-rechazada{background:rgba(255,143,163,.16);color:#ff9caf;border:1px solid rgba(255,143,163,.24)}
    .st-cancelada{background:rgba(255,143,163,.10);color:#f0a7b5;border:1px solid rgba(255,143,163,.18)}
    .st-surtiendo{background:rgba(244,190,89,.16);color:#ffd978;border:1px solid rgba(244,190,89,.24)}
    .st-en_ruta{background:rgba(120,205,255,.16);color:#9bd6ff;border:1px solid rgba(120,205,255,.24)}
    .st-entregada{background:rgba(83,224,158,.16);color:#85efba;border:1px solid rgba(83,224,158,.24)}

    .metric-card{
      border-radius:22px;border:1px solid rgba(160,198,255,.16);
      background:linear-gradient(180deg, rgba(8,25,60,.96), rgba(4,18,42,.94));
      padding:18px;min-height:128px
    }
    .metric-label{color:#b8ccef;font-weight:700}
    .metric-value{color:#fff;font-size:2rem;font-weight:800}

    .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)}

    .log-item{
      border:1px solid rgba(160,197,255,.10);background:rgba(255,255,255,.035);
      border-radius:16px;padding:12px 14px
    }

    .stock-card{
      border:1px solid rgba(160,197,255,.10);background:rgba(255,255,255,.035);
      border-radius:18px;padding:14px;height:100%
    }
    .stock-title{color:#fff;font-weight:700}
    .stock-sub{color:#abc2e6;font-size:.88rem}
    .stock-value{font-size:1.6rem;font-weight:800;color:#7cf0ae}

    .pill-active,.pill-inactive{
      display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;font-size:.8rem;font-weight:800
    }
    .pill-active{background:rgba(114,240,165,.16);color:#7cf0ae;border:1px solid rgba(114,240,165,.24)}
    .pill-inactive{background:rgba(255,143,163,.16);color:#ff9caf;border:1px solid rgba(255,143,163,.24)}

    @media (max-width: 991.98px){
      .brand-title{font-size:1.45rem}
      .page-wrap{padding:18px 12px 28px}
      .panel-pad{padding:16px}
      .metric-value{font-size:1.5rem}
    }


    /* ============================================================
       TEMA CLARO · Ventas
       ============================================================ */
    :root{--bg1:#f4f7fb;--bg2:#eef3f9;--line:#d8e2ef;--txt:#1f2937;--txt-strong:#0f172a;--txt-soft:#64748b;--gold:#d99a20;--green:#138a52;--danger:#c24155;--info:#2563eb;--shadow:0 18px 42px rgba(15,23,42,.10);--radius:22px;}
    body{color:var(--txt);background:radial-gradient(circle at 12% 18%, rgba(37,99,235,.10), transparent 24%),radial-gradient(circle at 88% 78%, rgba(14,165,233,.10), transparent 22%),linear-gradient(135deg, #f8fafc 0%, #eef4fb 100%);background-attachment:fixed;}
    .topbar{background:rgba(255,255,255,.92)!important;border-bottom:1px solid #e2e8f0!important;box-shadow:0 10px 28px rgba(15,23,42,.08)!important;}
    .brand-title{color:#0f172a!important}.brand-sub,.muted,.small,.small-note,.form-label{color:#64748b!important}
    .glass,.metric-card,.log-item,.stock-card{background:#ffffff!important;border:1px solid #dbe5f1!important;box-shadow:0 16px 36px rgba(15,23,42,.08)!important;color:#1f2937!important;}
    .section-title,.fw-bold,.fw-semibold,.stock-title{color:#0f172a!important}
    .toolbar-btn{background:#ffffff!important;border:1px solid #cbd5e1!important;color:#1e293b!important;box-shadow:0 8px 20px rgba(15,23,42,.06);}
    .toolbar-btn:hover{background:#f1f5f9!important;color:#0f172a!important}.btn-primary-soft{background:linear-gradient(135deg,#2563eb,#0ea5e9)!important;color:#fff!important;box-shadow:0 12px 24px rgba(37,99,235,.22)!important}.btn-green-soft{background:linear-gradient(135deg,#138a52,#22c55e)!important;color:#fff!important;box-shadow:0 12px 24px rgba(20,150,90,.20)!important}.btn-danger-soft{background:linear-gradient(135deg,#be123c,#fb7185)!important;color:#fff!important;box-shadow:0 12px 24px rgba(190,18,60,.18)!important}.btn-warning-soft{background:linear-gradient(135deg,#d97706,#fbbf24)!important;color:#fff!important;box-shadow:0 12px 24px rgba(217,119,6,.20)!important}
    .table-wrap{background:#ffffff!important;border:1px solid #dbe5f1!important;border-radius:20px!important}.table{--bs-table-bg:#ffffff;--bs-table-hover-bg:#f8fafc;--bs-table-border-color:#e2e8f0;color:#1f2937!important}.table thead th{background:#eff6ff!important;color:#0f172a!important;border-bottom:1px solid #dbe5f1!important}.table tbody td{color:#1f2937!important;border-color:#e2e8f0!important}.mono{color:#0f172a!important}
    .form-control,.form-select{color:#0f172a!important;background:#ffffff!important;border:1px solid #cbd5e1!important;box-shadow:none!important}.form-control::placeholder{color:#94a3b8!important}.form-control:focus,.form-select:focus{color:#0f172a!important;background:#ffffff!important;border-color:#3b82f6!important;box-shadow:0 0 0 .22rem rgba(59,130,246,.16)!important}.form-select option,.form-select optgroup{color:#0f172a;background:#ffffff}.form-control::-webkit-calendar-picker-indicator{filter:none!important}
    .modal-content{background:#ffffff!important;color:#1f2937!important;border:1px solid #dbe5f1!important;box-shadow:0 24px 64px rgba(15,23,42,.18)!important}.modal-header,.modal-footer{border-color:#e2e8f0!important}.btn-close-white{filter:none!important}
    .metric-label{color:#64748b!important}.metric-value{color:#0f172a!important}.stock-sub{color:#64748b!important}.stock-value{color:#138a52!important}.badge-status{border-width:1px;background:#f8fafc;color:#334155}
    .st-pendiente{background:#f1f5f9!important;color:#334155!important;border-color:#cbd5e1!important}.st-revision{background:#dbeafe!important;color:#1d4ed8!important;border-color:#93c5fd!important}.st-autorizada{background:#dcfce7!important;color:#15803d!important;border-color:#86efac!important}.st-rechazada,.st-cancelada{background:#ffe4e6!important;color:#be123c!important;border-color:#fecdd3!important}.st-surtiendo{background:#fef3c7!important;color:#b45309!important;border-color:#fde68a!important}.st-en_ruta{background:#e0f2fe!important;color:#0369a1!important;border-color:#bae6fd!important}.st-entregada{background:#d1fae5!important;color:#047857!important;border-color:#a7f3d0!important}.pill-active{background:#dcfce7!important;color:#15803d!important;border-color:#86efac!important}.pill-inactive{background:#ffe4e6!important;color:#be123c!important;border-color:#fecdd3!important}
    .view-tabs .btn{border-radius:999px;font-weight:800;padding:.65rem 1rem}.view-tabs .btn.active{background:#0f172a!important;color:#ffffff!important;border-color:#0f172a!important}.alert-soft{background:#f8fafc;border:1px solid #dbe5f1;border-radius:18px;padding:14px;color:#334155}

  
    /* =========================================================
       Bandeja comercial MES
    ========================================================= */
    .sales-ai{
      background:#fff;
      border:1px solid #dbe6f2;
      border-radius:22px;
      padding:18px;
      box-shadow:0 14px 34px rgba(16,36,62,.08);
      color:#10243e;
    }
    .ai-kpis{
      display:grid;
      grid-template-columns:repeat(4,minmax(130px,1fr));
      gap:12px;
      margin-top:14px;
    }
    .ai-kpi{
      border:1px solid #e1ebf5;
      border-radius:16px;
      background:#f8fbff;
      padding:12px 14px;
    }
    .ai-kpi-label{font-size:.75rem;color:#64748b;text-transform:uppercase;font-weight:800;}
    .ai-kpi-value{font-size:1.6rem;color:#10243e;font-weight:900;line-height:1.15;margin-top:5px;}
    .ai-body{
      display:grid;
      grid-template-columns:minmax(280px,1.1fr) minmax(260px,.9fr) minmax(260px,.9fr);
      gap:12px;
      margin-top:14px;
    }
    .ai-box{border:1px solid #e1ebf5;border-radius:16px;padding:13px;background:#fff;min-height:150px;}
    .ai-box-title{font-weight:900;color:#10243e;margin-bottom:10px;}
    .ai-alert{padding:10px;border-radius:12px;margin-bottom:8px;border:1px solid transparent;color:#334155;font-size:.84rem;}
    .ai-alert:last-child{margin-bottom:0;}
    .ai-alert.info{background:#eff6ff;border-color:#bfdbfe;}
    .ai-alert.warning{background:#fff7e6;border-color:#f3d39b;}
    .ai-alert.critical{background:#fff1f3;border-color:#f0b5c0;}
    .ai-alert.success{background:#ecfdf3;border-color:#a6e7c0;}
    .ai-alert-title{font-weight:900;color:#10243e;}
    .ai-row{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px solid #eef3f8;font-size:.83rem;}
    .ai-row:last-child{border-bottom:0;}

    .sales-ai-modal-content{border-radius:24px;overflow:hidden;}
    .sales-ai-modal-header{background:linear-gradient(135deg,#0f3d7a,#0ea5e9)!important;color:#fff!important;}
    .ai-floating-btn{
      position:fixed;
      right:26px;
      bottom:26px;
      z-index:1040;
      width:72px;
      height:72px;
      border:0;
      border-radius:24px;
      color:#fff;
      background:linear-gradient(135deg,#7c3aed,#0ea5e9);
      box-shadow:0 18px 42px rgba(37,99,235,.32),0 8px 18px rgba(124,58,237,.22);
      display:flex;
      flex-direction:column;
      align-items:center;
      justify-content:center;
      gap:1px;
      font-weight:900;
      transition:transform .18s ease, box-shadow .18s ease, filter .18s ease;
    }
    .ai-floating-btn:hover{transform:translateY(-3px) scale(1.03);filter:saturate(1.08);box-shadow:0 24px 54px rgba(37,99,235,.40),0 10px 22px rgba(124,58,237,.28);}
    .ai-floating-icon{font-size:1.45rem;line-height:1;}
    .ai-floating-text{font-size:.78rem;letter-spacing:.04em;}
    .ai-floating-badge{
      position:absolute;
      top:-7px;
      right:-7px;
      min-width:26px;
      height:26px;
      padding:0 7px;
      border-radius:999px;
      display:inline-flex;
      align-items:center;
      justify-content:center;
      background:#ef4444;
      color:#fff;
      border:3px solid #fff;
      font-size:.72rem;
      box-shadow:0 8px 18px rgba(239,68,68,.28);
    }
    .ai-floating-badge.is-zero{background:#16a34a;}
    .ai-row-title{font-weight:800;color:#19324d;}
    .ai-row-sub{font-size:.76rem;color:#64748b;}
    .ai-tag{padding:4px 8px;border-radius:999px;background:#eef6ff;color:#1153c7;font-weight:800;white-space:nowrap;}
    .ai-tag.warning{background:#fff3d6;color:#8a5b04;}
    .ai-tag.critical{background:#fee2e2;color:#b91c1c;}

    .mailbox{
      display:grid;
      grid-template-columns:230px minmax(320px,420px) minmax(360px,1fr);
      min-height:600px;
      border:1px solid #dbe6f2;
      border-radius:22px;
      overflow:hidden;
      background:#fff;
      box-shadow:0 14px 34px rgba(16,36,62,.08);
    }
    .mailbox-folders{padding:16px 12px;background:#f5f9fe;border-right:1px solid #dbe6f2;}
    .mail-folder{
      display:flex;align-items:center;justify-content:space-between;gap:8px;
      width:100%;border:0;border-radius:14px;padding:12px 10px;margin-bottom:7px;
      background:transparent;color:#19324d;font-weight:800;text-align:left;
    }
    .mail-folder:hover,.mail-folder.active{background:#e8f1ff;color:#1153c7;}
    .mail-count{
      display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:25px;
      border-radius:999px;padding:0 8px;background:#e3ebf6;font-size:.82rem;color:#19324d;
    }
    .mail-folder.active .mail-count{background:#2563eb;color:#fff;}
    .mail-scope-note{
      margin-top:18px;border:1px solid #c9dcf6;border-radius:13px;
      padding:11px;background:#eef6ff;color:#425b76;font-size:.82rem;
    }
    .mail-list{border-right:1px solid #dbe6f2;min-width:0;background:#fff;}
    .mail-list-title{padding:16px;border-bottom:1px solid #e6edf5;}
    .mail-items{max-height:535px;overflow:auto;}
    .mail-item{
      width:100%;padding:15px 16px;border:0;border-bottom:1px solid #eef3f8;
      text-align:left;background:#fff;color:#10243e;
    }
    .mail-item:hover,.mail-item.active{background:#f1f7ff;}
    .mail-item.active{box-shadow:inset 3px 0 0 #2563eb;}
    .mail-folio{font-weight:900;color:#122b48;}
    .mail-sub{font-size:.84rem;color:#64748b;margin-top:4px;}
    .mail-bottom{display:flex;justify-content:space-between;gap:8px;margin-top:8px;font-size:.8rem;color:#586e88;}
    .mail-detail{padding:20px;background:#fff;min-width:0;}
    .mail-empty{min-height:370px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:#64748b;padding:28px;}
    .mail-detail-card{border:1px solid #e1ebf5;background:#f8fbff;border-radius:14px;padding:12px;}
    .mail-detail-label{font-size:.71rem;color:#64748b;font-weight:800;text-transform:uppercase;}
    .mail-detail-value{font-weight:800;color:#10243e;margin-top:4px;}
    .mail-status{display:inline-flex;padding:5px 10px;border-radius:999px;font-size:.73rem;font-weight:900;}
    .mail-status.pending{background:#fff3d6;color:#8a5b04;}
    .mail-status.review{background:#e8f1ff;color:#1153c7;}
    .mail-status.authorized{background:#def8e9;color:#087443;}
    .mail-status.shipping{background:#ede9fe;color:#6d28d9;}
    .mail-status.done{background:#d1fae5;color:#065f46;}
    .mail-status.cancelled{background:#fee2e2;color:#b91c1c;}

    @media (max-width:1199.98px){
      .ai-kpis{grid-template-columns:repeat(2,minmax(150px,1fr));}
      .ai-body{grid-template-columns:1fr;}
      .mailbox{grid-template-columns:215px 1fr;}
      .mail-detail{grid-column:1/-1;border-top:1px solid #dbe6f2;}
    }
    @media (max-width:767.98px){
      .ai-kpis{grid-template-columns:1fr;}
      .mailbox{display:block;min-height:0;}
      .mailbox-folders{display:flex;gap:7px;flex-wrap:wrap;border-right:0;border-bottom:1px solid #dbe6f2;}
      .mail-folder{width:auto;margin-bottom:0;}
      .mail-scope-note{width:100%;margin-top:8px;}
      .mail-list{border-right:0;}
      .mail-items{max-height:390px;}
    }

  
  /* ============================================================
     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;
  }


  /* Barra de acciones y notas del usuario en bandeja de pedidos */
  .user-notes-card{
    border:1px solid #cfe5ff;
    background:linear-gradient(135deg,#f8fbff,#eef7ff);
    border-radius:16px;
    padding:12px 14px;
  }
  .user-notes-card .note-text{
    font-weight:800;
    color:#12345d;
    white-space:pre-wrap;
    word-break:break-word;
  }
  .order-action-bar{
    position:sticky;
    bottom:0;
    z-index:8;
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:10px;
    flex-wrap:wrap;
    padding:10px;
    border:1px solid #dbe6f2;
    border-radius:18px;
    background:rgba(255,255,255,.96);
    box-shadow:0 16px 32px rgba(15,23,42,.10);
    backdrop-filter:blur(10px);
  }
  .order-action-title{
    font-size:.74rem;
    text-transform:uppercase;
    letter-spacing:.04em;
    font-weight:900;
    color:#64748b;
  }
  .order-action-buttons{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;}
  .order-action-menu{display:none;}
  .order-action-menu .dropdown-menu{
    border:1px solid #dbe6f2;
    border-radius:16px;
    box-shadow:0 18px 44px rgba(15,23,42,.18);
    padding:8px;
    min-width:230px;
  }
  .order-action-menu .dropdown-item{
    border-radius:12px;
    padding:10px 12px;
    font-weight:800;
  }
  @media (max-width:991.98px){
    .order-action-bar{position:sticky;bottom:8px;margin-top:10px;}
    .order-action-buttons{display:none;}
    .order-action-menu{display:block;margin-left:auto;}
  }
  @media (max-width:575.98px){
    .order-action-bar{align-items:center;padding:9px;border-radius:16px;}
    .order-action-title{font-size:.68rem;}
    .user-notes-card{padding:10px 12px;border-radius:14px;}
  }


.route-stock-panel{background:#f8fbff;border:1px solid #dbeafe;border-radius:16px;padding:12px}
.route-stock-box{max-height:170px;overflow:auto;border-radius:12px}
.route-stock-box table{margin:0}
.route-stock-current{background:#ecfdf5;border:1px solid #bbf7d0;color:#166534;border-radius:999px;padding:3px 8px;font-size:.75rem;font-weight:800}
.route-stock-empty{background:#f8fafc;border:1px dashed #cbd5e1;border-radius:12px;padding:10px;color:#64748b}
</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 · Botella -  ventas</div>
      <div class="brand-sub">Ordenes de salida desde ventas</div>
    </div>

    <div class="ms-auto d-flex align-items-center gap-2 flex-wrap">
      <span class="muted small"><?= h($u['full_name'] ?? '') ?></span>
      <a class="btn toolbar-btn btn-sm" href="dashboard.php">Dashboard</a>
      <a class="btn toolbar-btn btn-sm" href="select_scope.php">Cambiar de empresa</a>
      <a class="btn toolbar-btn btn-sm" href="logout.php">Salir</a>
    </div>
  </div>
</nav>

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

    <div class="glass panel-pad mb-4">
      <div class="d-flex flex-wrap gap-3 align-items-end justify-content-between">
        <div>
          <div class="section-title mb-2">
            <i class="bi bi-bag-check-fill text-warning"></i>
            Centro comercial de pedidos
          </div>
        </div>

        <div class="d-flex gap-2 flex-wrap">
          <button id="btnNewWarehouseOrder" class="btn btn-green-soft" type="button">
            <i class="bi bi-plus-circle me-1"></i>Nueva orden de salida
          </button>
          <button id="btnOpenInventory" class="btn btn-warning-soft" type="button">
            <i class="bi bi-box-seam me-1"></i>Existencias
          </button>
          <button id="btnOpenCustomers" class="btn toolbar-btn" type="button">
            <i class="bi bi-people me-1"></i>Clientes
          </button>
          <button id="btnOpenProducts" class="btn toolbar-btn" type="button">
            <i class="bi bi-boxes me-1"></i>Artículos
          </button>
          <button id="btnReload" class="btn toolbar-btn" type="button">
            <i class="bi bi-arrow-repeat me-1"></i>Actualizar
          </button>
        </div>
      </div>

      <div class="row g-3 mt-3">
        <div class="col-12 col-md-3">
          <label class="form-label">Fecha desde</label>
          <input id="fDateFrom" type="date" class="form-control">
        </div>
        <div class="col-12 col-md-3">
          <label class="form-label">Fecha hasta</label>
          <input id="fDateTo" type="date" class="form-control">
        </div>
        <div class="col-12 col-md-3">
          <label class="form-label">Cliente / folio</label>
          <input id="fCustomer" class="form-control" placeholder="Buscar cliente o pedido">
        </div>
        <div class="col-12 col-md-3">
          <label class="form-label">Planta / ubicación</label>
          <select id="fLocation" class="form-select"></select>
        </div>
      </div>
      <input id="fStatus" type="hidden" value="">
    </div>

    <button class="ai-floating-btn" id="btnOpenSalesAi" type="button" title="Abrir diagnóstico inteligente">
      <span class="ai-floating-icon"><i class="bi bi-stars"></i></span>
      <span class="ai-floating-text">IA</span>
      <span class="ai-floating-badge" id="aiFloatingRisks">0</span>
    </button>

    <div class="mailbox">
      <aside class="mailbox-folders">
        <button type="button" class="mail-folder active" data-mail-folder="all">
          <span><i class="bi bi-inboxes me-2"></i>Todos</span><span class="mail-count" id="mailCountAll">0</span>
        </button>
        <button type="button" class="mail-folder" data-mail-folder="review">
          <span><i class="bi bi-search me-2"></i>Por revisar</span><span class="mail-count" id="mailCountReview">0</span>
        </button>
        <button type="button" class="mail-folder" data-mail-folder="authorized">
          <span><i class="bi bi-check-circle me-2"></i>Autorizadas</span><span class="mail-count" id="mailCountAuthorized">0</span>
        </button>
        <button type="button" class="mail-folder" data-mail-folder="shipping">
          <span><i class="bi bi-truck me-2"></i>En proceso</span><span class="mail-count" id="mailCountShipping">0</span>
        </button>
        <button type="button" class="mail-folder" data-mail-folder="done">
          <span><i class="bi bi-check2-all me-2"></i>Entregadas</span><span class="mail-count" id="mailCountDone">0</span>
        </button>
        <button type="button" class="mail-folder" data-mail-folder="cancelled">
          <span><i class="bi bi-x-circle me-2"></i>Canceladas</span><span class="mail-count" id="mailCountCancelled">0</span>
        </button>
        <div class="mail-scope-note" id="mailInventoryNote">
          Inventario por almacenes surtibles de la planta activa.
        </div>
      </aside>

      <section class="mail-list">
        <div class="mail-list-title">
          <div class="fw-bold" id="mailFolderTitle">Todos los pedidos</div>
          <div class="small muted" id="mailFolderSub">0 registros</div>
        </div>
        <div class="mail-items" id="mailItems"></div>
      </section>

      <section class="mail-detail" id="mailDetail">
        <div class="mail-empty">
          <i class="bi bi-envelope-open fs-1 mb-2"></i>
          <div class="fw-bold">Selecciona un pedido</div>
          <div class="small">Consulta sus artículos, estatus y acciones comerciales.</div>
        </div>
      </section>
    </div>

    <div class="d-none">
      <button id="tabActive" type="button"></button>
      <button id="tabHistory" type="button"></button>
      <span id="kTotal"></span><span id="kPending"></span><span id="kApproved"></span>
      <div id="emptyHint"></div><table><tbody id="tbOrders"></tbody></table>
    </div>

  </div>
</div>

<!-- Modal detalle -->

<div class="modal fade" id="salesAiModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
    <div class="modal-content sales-ai-modal-content">
      <div class="modal-header sales-ai-modal-header">
        <div>
          <div class="fw-bold fs-5"><i class="bi bi-stars me-2 text-warning"></i>Diagnóstico inteligente de ventas</div>
          <div class="small opacity-75">
            Pedidos, disponibilidad surtible y clientes activos de la planta seleccionada.
            <span class="mono text-white" id="aiRange"></span>
          </div>
        </div>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>
      </div>
      <div class="modal-body">
        <div class="d-flex align-items-center justify-content-between gap-2 flex-wrap mb-3">
          <div class="small muted">Análisis operativo según filtros actuales de fecha y planta.</div>
          <button class="btn toolbar-btn btn-sm" id="btnReloadInsights" type="button">
            <i class="bi bi-arrow-repeat me-1"></i>Actualizar análisis
          </button>
        </div>

        <div class="sales-ai">
          <div class="ai-kpis">
            <div class="ai-kpi">
              <div class="ai-kpi-label">Pedidos del periodo</div>
              <div class="ai-kpi-value mono" id="aiOrders">0</div>
            </div>
            <div class="ai-kpi">
              <div class="ai-kpi-label">Por surtir</div>
              <div class="ai-kpi-value mono" id="aiDispatch">0</div>
            </div>
            <div class="ai-kpi">
              <div class="ai-kpi-label">Por revisar</div>
              <div class="ai-kpi-value mono" id="aiReview">0</div>
            </div>
            <div class="ai-kpi">
              <div class="ai-kpi-label">Riesgos de inventario</div>
              <div class="ai-kpi-value mono" id="aiRisks">0</div>
            </div>
          </div>
          <div class="ai-body">
            <div class="ai-box">
              <div class="ai-box-title"><i class="bi bi-lightbulb me-1"></i>Sugerencias comerciales</div>
              <div id="aiSuggestions"><div class="small muted">Abre el diagnóstico para analizar pedidos...</div></div>
            </div>
            <div class="ai-box">
              <div class="ai-box-title"><i class="bi bi-exclamation-triangle me-1"></i>Riesgo de surtido</div>
              <div id="aiRiskRows"><div class="small muted">Sin datos todavía.</div></div>
            </div>
            <div class="ai-box">
              <div class="ai-box-title"><i class="bi bi-people me-1"></i>Clientes con actividad</div>
              <div id="aiCustomers"><div class="small muted">Sin datos todavía.</div></div>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" 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-xl modal-dialog-scrollable modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <div>
          <div class="fw-bold fs-5">Detalle de pedido</div>
          <div class="muted small">Consulta completa del pedido y acciones de ventas.</div>
        </div>
        <button type="button" class="btn-close btn-close-white" id="btnNewOrderHeaderClose" aria-label="Cerrar"></button>
      </div>

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

        <div class="glass panel-pad mb-4">
          <div class="section-title mb-3">
            <i class="bi bi-box-seam text-warning"></i>
            Artículos del pedido
          </div>

          <div class="table-wrap">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>OC</th>
                    <th>Artículo</th>
                    <th>Embalaje</th>
                    <th class="text-end">Tarimas</th>
                    <th class="text-end">Paq/Tar</th>
                    <th class="text-end">Pz/Paq</th>
                    <th class="text-end">Paquetes</th>
                    <th class="text-end">Botellas</th>
                  </tr>
                </thead>
                <tbody id="detailItems"></tbody>
              </table>
            </div>
          </div>
        </div>

        <div class="glass panel-pad mb-4">
          <div class="section-title mb-3">
            <i class="bi bi-clock-history text-warning"></i>
            Bitácora de estatus
          </div>
          <div class="d-grid gap-2" id="detailLogs"></div>
        </div>

        <div class="glass panel-pad">
          <div class="section-title mb-3">
            <i class="bi bi-pencil-square text-warning"></i>
            Acción de ventas
          </div>

          <div class="row g-3">
            <div class="col-12 col-md-4">
              <label class="form-label">Nuevo estatus</label>
              <select id="mNewStatus" class="form-select">
                <option value="">Selecciona</option>
                <option value="EN_REVISION">Enviar a revisión</option>
                <option value="AUTORIZADA">Autorizar</option>
                <option value="RECHAZADA">Rechazar</option>
                <option value="CANCELADA">Cancelar</option>
              </select>
            </div>
            <div class="col-12 col-md-8">
              <label class="form-label">Comentario</label>
              <input id="mComment" class="form-control" placeholder="Motivo o comentario de seguimiento">
            </div>
          </div>
        </div>
      </div>

      <div class="modal-footer">
        <input type="hidden" id="mOrderId">
        <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button>
        <button type="button" id="btnSaveStatus" class="btn btn-primary-soft">Guardar cambio</button>
      </div>
    </div>
  </div>
</div>

<!-- Modal existencias -->
<div class="modal fade" id="inventoryModal" 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">Existencias disponibles</div>
          <div class="muted small">Consulta de inventario comercial por ubicació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">
          <div class="col-12 col-md-5">
            <label class="form-label">Ubicación</label>
            <select id="invLocationId" class="form-select"></select>
          </div>
          <div class="col-12 col-md-3">
            <label class="form-label">Acción</label>
            <button id="btnLoadInventory" type="button" class="btn btn-warning-soft w-100">
              <i class="bi bi-search me-1"></i>Consultar
            </button>
          </div>
          <div class="col-12 col-md-4 d-flex align-items-end">
            <div class="small-note w-100" id="invLocationInfo">
              Selecciona una ubicación para consultar existencias.
            </div>
          </div>
        </div>

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

        <div class="glass panel-pad">
          <div class="section-title mb-3">
            <i class="bi bi-box-seam text-warning"></i>
            Listado de artículos disponibles
          </div>

          <div class="table-wrap">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>SKU</th>
                    <th>Producto</th>
                    <th class="text-end">Disponible</th>
                    <th>UOM</th>
                    <th>Estatus</th>
                  </tr>
                </thead>
                <tbody id="invStockBody"></tbody>
              </table>
            </div>
          </div>

          <div class="muted small mt-3" id="invEmptyHint"></div>
        </div>
      </div>

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

<!-- Modal clientes -->
<div class="modal fade" id="customersModal" 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">Clientes comerciales</div>
          <div class="muted small">Activar o desactivar clientes disponibles para operación comercial.</div>
        </div>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="d-flex justify-content-between align-items-end gap-2 flex-wrap mb-3">
          <div class="row g-3 flex-grow-1">
            <div class="col-12 col-md-8">
            <label class="form-label">Buscar cliente</label>
            <input id="customerSearch" class="form-control" placeholder="Nombre comercial, contacto o teléfono">
            </div>
          </div>
          <button id="btnNewCustomer" class="btn btn-primary-soft" type="button"><i class="bi bi-person-plus me-1"></i>Nuevo cliente</button>
        </div>

        <div class="table-wrap">
          <div class="table-responsive">
            <table class="table table-hover align-middle">
              <thead>
                <tr>
                  <th>No. cliente</th>
                  <th>Nombre del cliente</th>
                  <th>Razón social</th>
                  <th>Punto entrega principal</th>
                  <th>Localidad</th>
                  <th>Estatus</th>
                  <th class="text-end">Accesos / direcciones</th>
                  <th class="text-end">Acción</th>
                </tr>
              </thead>
              <tbody id="tbCustomers"></tbody>
            </table>
          </div>
        </div>
        <div class="muted small mt-3" id="customersEmptyHint"></div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button>
      </div>
    </div>
  </div>
</div>


<!-- Modal alta / edición cliente -->
<div class="modal fade" id="customerEditModal" 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" id="customerEditTitle">Cliente</div><div class="muted small">Clientes por empresa y localidad. El número de cliente viene del sistema principal de ventas.</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="ceCustomerId"><div class="row g-3">
      <div class="col-12 col-md-6"><label class="form-label">Localidad</label><select id="ceLocationId" class="form-select"></select></div>
      <div class="col-12 col-md-3"><label class="form-label">Número de cliente</label><input id="ceCustomerCode" class="form-control mono" placeholder="Ej. 84782"></div>
      <div class="col-12 col-md-3"><label class="form-label">Estatus</label><select id="ceActive" class="form-select"><option value="1">Activo</option><option value="0">Inactivo</option></select></div>
      <div class="col-12 col-md-6"><label class="form-label">Nombre del cliente</label><input id="ceBusinessName" class="form-control" placeholder="Ej. CASINO NEW YORK"></div>
      <div class="col-12 col-md-6"><label class="form-label">Razón social</label><input id="ceContactName" class="form-control" placeholder="Ej. BEXAR GESTION DE NEGOCIOS"></div>
      <div class="col-12 col-md-4"><label class="form-label">Teléfono</label><input id="cePhone" class="form-control"></div>
      <div class="col-12 col-md-4"><label class="form-label">Correo</label><input id="ceEmail" class="form-control"></div>
      <div class="col-12 col-md-4"><label class="form-label">C.P.</label><input id="cePostalCode" class="form-control"></div>
      <div class="col-12"><label class="form-label">Punto de entrega principal</label><input id="ceAddress" class="form-control" placeholder="Calle, colonia, ciudad, C.P."></div>
    </div><div id="customerEditResult" class="mt-3"></div></div>
    <div class="modal-footer"><button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button><button type="button" id="btnCustomerSave" class="btn btn-green-soft"><i class="bi bi-save me-1"></i>Guardar cliente</button></div>
  </div></div>
</div>

<!-- Modal direcciones cliente -->
<div class="modal fade" id="customerAddressesModal" 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">Puntos de entrega</div><div class="muted small" id="caTitleSub">Registra diferentes destinos para el cliente por localidad.</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="caCustomerId"><input type="hidden" id="caAddressId">
      <div class="glass panel-pad mb-4"><div class="row g-3">
        <div class="col-12 col-md-4"><label class="form-label">Nombre del punto de entrega</label><input id="caLabel" class="form-control" placeholder="Ej. CASINO NEW YORK"></div>
        <div class="col-12 col-md-4"><label class="form-label">Localidad</label><select id="caLocationId" class="form-select"></select></div>
        <div class="col-12 col-md-2"><label class="form-label">C.P.</label><input id="caPostalCode" class="form-control"></div>
        <div class="col-12 col-md-1"><label class="form-label">Default</label><select id="caDefault" class="form-select"><option value="0">No</option><option value="1">Sí</option></select></div>
        <div class="col-12 col-md-1"><label class="form-label">Activo</label><select id="caActive" class="form-select"><option value="1">Sí</option><option value="0">No</option></select></div>
        <div class="col-12"><label class="form-label">Punto de entrega / dirección</label><input id="caAddress" class="form-control" placeholder="Calle, colonia, ciudad, C.P."></div>
        <div class="col-12 d-flex gap-2"><button id="btnCustomerAddressSave" type="button" class="btn btn-green-soft"><i class="bi bi-save me-1"></i>Guardar punto de entrega</button><button id="btnCustomerAddressClear" type="button" class="btn toolbar-btn">Limpiar</button></div>
      </div></div>
      <div class="table-wrap"><div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>Punto entrega</th><th>Localidad</th><th>Dirección</th><th>C.P.</th><th>Default</th><th>Estatus</th><th class="text-end">Acciones</th></tr></thead><tbody id="tbCustomerAddresses"></tbody></table></div></div><div class="muted small mt-3" id="customerAddressesEmptyHint"></div>
    </div><div class="modal-footer"><button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button></div>
  </div></div>
</div>

<!-- Modal accesos cliente -->
<div class="modal fade" id="customerUsersModal" 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">Accesos del cliente</div>
          <div class="muted small" id="customerUsersTitleSub">Alta, edición, contraseña y activación del acceso del cliente.</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="cuCustomerId">
        <input type="hidden" id="cuEditingUserId">

        <div class="glass panel-pad mb-4">
          <div class="section-title mb-3"><i class="bi bi-person-plus-fill text-warning"></i> Usuario cliente</div>
          <div class="row g-3">
            <div class="col-12 col-md-4">
              <label class="form-label">Nombre</label>
              <input id="cuFullName" class="form-control" placeholder="Nombre del usuario cliente">
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Teléfono / usuario</label>
              <input id="cuPhone" class="form-control" placeholder="Ej. 8991234567">
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Correo</label>
              <input id="cuEmail" class="form-control" placeholder="Opcional">
            </div>
            <div class="col-12 col-md-2">
              <label class="form-label">Rol</label>
              <select id="cuRole" class="form-select">
                <option value="cliente">Cliente</option>
                <option value="tienda">Tienda</option>
              </select>
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Contraseña</label>
              <input id="cuPassword" type="text" class="form-control" placeholder="Mínimo 4 caracteres">
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Estatus</label>
              <select id="cuActive" class="form-select">
                <option value="1">Activo</option>
                <option value="0">Inactivo</option>
              </select>
            </div>
            <div class="col-12 col-md-6 d-flex align-items-end gap-2">
              <button id="btnCustomerUserSave" type="button" class="btn btn-primary-soft">
                <i class="bi bi-save me-1"></i>Guardar usuario
              </button>
              <button id="btnCustomerUserClear" type="button" class="btn toolbar-btn">
                <i class="bi bi-eraser me-1"></i>Limpiar
              </button>
            </div>
          </div>
          <div class="muted small mt-3">El teléfono será el usuario de acceso. En edición puedes actualizar datos, cambiar contraseña y habilitar o deshabilitar el acceso.</div>
        </div>

        <div class="glass panel-pad">
          <div class="section-title mb-3"><i class="bi bi-people-fill text-warning"></i> Usuarios registrados</div>
          <div class="table-wrap">
            <div class="table-responsive">
              <table class="table table-hover align-middle">
                <thead>
                  <tr>
                    <th>Nombre</th>
                    <th>Usuario</th>
                    <th>Rol</th>
                    <th>Correo</th>
                    <th>Estatus</th>
                    <th>Último acceso</th>
                    <th class="text-end">Acciones</th>
                  </tr>
                </thead>
                <tbody id="tbCustomerUsers"></tbody>
              </table>
            </div>
          </div>
          <div class="muted small mt-3" id="customerUsersEmptyHint"></div>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button>
      </div>
    </div>
  </div>
</div>

<!-- Modal productos -->
<div class="modal fade" id="productsModal" 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">Artículos comerciales</div>
          <div class="muted small">Activar o desactivar artículos visibles para operación comercial.</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">
            <label class="form-label">Buscar artículo</label>
            <input id="productSearch" class="form-control" placeholder="SKU o nombre del artículo">
          </div>
        </div>

        <div class="table-wrap">
          <div class="table-responsive">
            <table class="table table-hover align-middle">
              <thead>
                <tr>
                  <th>SKU</th>
                  <th>Artículo</th>
                  <th>UOM</th>
                  <th>Estatus</th>
                  <th class="text-end">Acción</th>
                </tr>
              </thead>
              <tbody id="tbProducts"></tbody>
            </table>
          </div>
        </div>
        <div class="muted small mt-3" id="productsEmptyHint"></div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button>
      </div>
    </div>
  </div>
</div>




<!-- Modal nueva orden de salida -->
<div class="modal fade" id="newOrderModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
  <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">Nueva orden logística · Botella</div>
          <div class="muted small">Captura solo los datos necesarios. El inventario se descontará únicamente cuando Almacén confirme la salida.</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="newOrderId">
        <div class="glass panel-pad mb-4">
          <div class="section-title mb-3">
            <i class="bi bi-clipboard-plus text-primary"></i>
            Datos de la orden
          </div>

          <div class="row g-3">
            <div class="col-12 col-md-4">
              <label class="form-label">Tipo de orden</label>
              <select id="newOrderType" class="form-select">
                <option value="CLIENTE">Salida a cliente · OSV</option>
                <option value="RUTA">Ruta de reparto · OSA</option>
              </select>
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Planta / ubicación</label>
              <select id="newOrderLocation" class="form-select"></select>
            </div>
            <div class="col-12 col-md-4 order-client-field header-oc-field d-none">
              <label class="form-label">OC base cliente (solo OSV)</label>
              <input id="newOrderPurchaseOrder" class="form-control mono" placeholder="Ej. 74112">
            </div>

            <div class="col-12 col-md-4 order-client-field header-oc-field d-none">
              <label class="form-label">Adjuntar OC cliente</label>
              <input id="newOrderOcFile" type="file" class="form-control" accept=".pdf,.jpg,.jpeg,.png,.webp">
            </div>

            <div class="col-12 col-md-4 order-client-field">
              <label class="form-label">Cliente registrado</label>
              <select id="newOrderCustomerId" class="form-select">
                <option value="">Selecciona cliente registrado</option>
              </select>
            </div>
            <div class="col-12 col-md-4 order-client-field">
              <label class="form-label">Punto de entrega</label>
              <select id="newOrderAddressId" class="form-select">
                <option value="">Selecciona punto de entrega</option>
              </select>
            </div>

            <div class="col-12 col-md-6 order-route-field d-none">
              <label class="form-label">Ruta de reparto</label>
              <select id="newOrderRoute" class="form-select">
                <option value="">Selecciona ruta configurada</option>
              </select>
            </div>
            <div class="col-12 col-md-6 order-route-field d-none">
              <label class="form-label">Ruta seleccionada</label>
              <input id="newOrderRouteLabel" class="form-control" value="Selecciona una ruta" readonly>
            </div>

            <div class="col-12 order-route-field d-none">
              <div id="newOrderRouteStockPanel" class="route-stock-panel">
                <div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
                  <div>
                    <div class="fw-bold"><i class="bi bi-truck text-primary me-1"></i>Inventario actual de la ruta</div>
                    <div class="small muted">Si la ruta ya trae producto, la nueva carga se suma al saldo del almacén móvil.</div>
                  </div>
                  <span id="newOrderRouteWarehouseCode" class="badge rounded-pill bg-light text-dark border">Selecciona ruta</span>
                </div>
                <div id="newOrderRouteStockBox" class="route-stock-box mt-2">Selecciona una ruta para consultar saldo.</div>
              </div>
            </div>

            <div class="col-12 order-client-field">
              <label class="form-label">Dirección / destino seleccionado</label>
              <input id="newOrderAddress" class="form-control" placeholder="Se llena al seleccionar punto de entrega; también puedes capturar manualmente">
            </div>

            <div class="col-12 col-md-3">
              <label class="form-label">Fecha de entrega</label>
              <input id="newOrderDate" type="date" class="form-control">
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Hora de entrega</label>
              <input id="newOrderTime" type="time" class="form-control">
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Unidad a cargar</label>
              <select id="newOrderVehicleUnit" class="form-select">
                <option value="Caja seca">Caja seca</option>
                <option value="Torton">Torton</option>
              </select>
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Capacidad / tipo unidad</label>
              <input id="newOrderVehicleCapacity" class="form-control mono" value="30 tarimas" readonly>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Notas para almacén / reparto</label>
              <input id="newOrderComments" class="form-control" placeholder="Observaciones adicionales">
            </div>
          </div>
        </div>

        <div class="glass panel-pad mb-4">
          <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
            <div class="section-title mb-0">
              <i class="bi bi-box-seam text-primary"></i>
              Artículos a cargar
            </div>
            <div class="badge rounded-pill bg-light text-dark border" id="newOrderCapacityBadge">0 / 30 tarimas</div>
          </div>

          <div class="row g-3 align-items-end">
            <div class="col-12 col-md-3 order-client-item-oc-field d-none">
              <label class="form-label">OC del artículo</label>
              <input id="newOrderItemPurchaseOrder" class="form-control mono" placeholder="OC">
            </div>
            <div class="d-none">
              <select id="newOrderOcRelation" class="form-select">
                <option value="MISMO_CLIENTE_MISMA_DIRECCION">Mismo cliente / misma dirección</option>
                <option value="MISMO_CLIENTE_OTRA_DIRECCION">Mismo cliente / otra dirección</option>
                <option value="OTRO_CLIENTE">Cliente diferente</option>
              </select>
            </div>
            <div class="col-12 col-md-3 order-client-item-oc-field order-client-item-oc-attachment-field d-none">
              <label class="form-label">Adjuntar OC de compra</label>
              <input id="newOrderItemOcFile" type="file" class="form-control" accept=".pdf,.jpg,.jpeg,.png,.webp">
              <div class="small muted mt-1">Solo se solicita una vez por cada OC.</div>
            </div>
            <div class="col-12 col-md-3 order-client-item-oc-field order-client-item-oc-attachment-field d-none">
              <label class="form-label">Motivo sin adjunto</label>
              <input id="newOrderItemOcMissingReason" class="form-control" placeholder="Opcional: correo, verbal, pendiente...">
              <div class="small muted mt-1">No bloquea la captura.</div>
            </div>
            <div class="col-12 col-md-6 order-client-item-oc-reuse-hint d-none">
              <div id="newOrderOcReuseHint" class="alert alert-success mb-0 py-2">
                Esta OC ya fue registrada. No es necesario volver a adjuntar el archivo.
              </div>
            </div>
            <div class="col-12 col-md-4 order-client-item-dest-field d-none">
              <label class="form-label">Cliente de esta OC</label>
              <select id="newOrderItemCustomerId" class="form-select"><option value="">Selecciona cliente</option></select>
            </div>
            <div class="col-12 col-md-4 order-client-item-dest-field d-none">
              <label class="form-label">Punto de entrega de esta OC</label>
              <select id="newOrderItemAddressId" class="form-select"><option value="">Selecciona punto</option></select>
            </div>
            <div class="col-12 col-md-4 order-client-item-dest-field d-none">
              <label class="form-label">Dirección de esta OC</label>
              <input id="newOrderItemAddress" class="form-control" readonly>
            </div>
            <div class="col-12 col-md-6">
              <label class="form-label">Artículo</label>
              <select id="newOrderProduct" class="form-select"></select>
            </div>
            <div class="col-12 col-md-5">
              <label class="form-label">Embalaje</label>
              <select id="newOrderPackaging" class="form-select"></select>
            </div>
            <div class="col-6 col-md-2">
              <label class="form-label">Tarimas</label>
              <input id="newOrderPalletsQty" type="number" min="1" step="1" class="form-control mono" placeholder="0">
            </div>
            <div class="col-6 col-md-2">
              <label class="form-label">Tipo tarima</label>
              <select id="newOrderPalletType" class="form-select"><option value="Chep">Chep</option><option value="Gris">Gris</option></select>
            </div>
            <div class="col-6 col-md-2">
              <label class="form-label">Especificación</label>
              <select id="newOrderPalletSpec" class="form-select"><option value="retorno">retorno</option><option value="surtido">surtido</option></select>
            </div>
            <div class="col-6 col-md-1 d-grid">
              <button id="btnNewOrderAddItem" type="button" class="btn btn-primary-soft"><i class="bi bi-plus-lg"></i></button>
            </div>
          </div>

          <div id="newOrderStockInfo" class="small muted mt-2"></div>
          <input id="newOrderCalcPreview" class="form-control mono mt-2" value="Paquetes / Botellas" readonly>
          <button id="btnNewOrderRefreshStock" type="button" class="btn btn-outline-primary btn-sm mt-2"><i class="bi bi-arrow-clockwise me-1"></i>Actualizar existencia</button>

          <div class="table-wrap mt-3">
            <div class="table-responsive">
              <table class="table table-hover align-middle mb-0">
                <thead><tr><th>OC</th><th>Artículo</th><th>Embalaje</th><th class="text-end">Saldo ruta</th><th class="text-end">Tarimas</th><th class="text-end">Paq/Tar</th><th class="text-end">Paquetes</th><th class="text-end">Botellas</th><th class="text-end">Total ruta</th><th></th></tr></thead>
                <tbody id="newOrderItemsBody"></tbody>
              </table>
            </div>
          </div>
          <div id="newOrderItemsHint" class="small muted mt-2"></div>
        </div>

        <div id="newOrderResult"></div>
      </div>

      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" id="btnNewOrderFooterClose">Cerrar</button>
        <button type="button" id="btnNewOrderSave" class="btn btn-green-soft">
          <i class="bi bi-check-circle me-1"></i>Crear orden
        </button>
      </div>
    </div>
  </div>
</div>

<!-- Modal advertencia cierre de orden sin guardar -->
<div class="modal fade" id="newOrderCloseConfirmModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <div>
          <div class="fw-bold fs-5">Cerrar solicitud sin guardar</div>
          <div class="muted small">Hay información capturada que todavía no se ha guardado.</div>
        </div>
      </div>
      <div class="modal-body">
        <div class="alert alert-warning mb-0">
          <i class="bi bi-exclamation-triangle me-2"></i>
          Si cierras esta solicitud se perderán los cambios capturados. ¿Deseas cerrarla sin guardar?
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" id="btnNewOrderCloseNo">No, continuar captura</button>
        <button type="button" class="btn btn-danger-soft" id="btnNewOrderCloseYes">Sí, cerrar sin guardar</button>
      </div>
    </div>
  </div>
</div>

<!-- Modal confirmar creación / edición de orden -->
<div class="modal fade" id="botellaPalletValidationModal" 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">Revisar cantidad de tarimas</div>
          <div class="muted small">Botella solo permite embarques con tarimas completas.</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="alert alert-warning mb-0">
          <i class="bi bi-exclamation-triangle me-2"></i>
          <span id="botellaPalletValidationText"></span>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-primary-soft" data-bs-dismiss="modal">Regresar y corregir</button>
      </div>
    </div>
  </div>
</div>

<!-- Modal aviso de existencia insuficiente · Botella -->
<div class="modal fade" id="newOrderNoStockModal" 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">Aviso de existencia</div>
          <div class="muted small">La orden se puede generar, pero requiere coordinación con Almacén / Producción.</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="alert alert-warning mb-0">
          <i class="bi bi-exclamation-triangle me-2"></i>
          <span id="newOrderNoStockText"></span>
        </div>
        <div class="small muted mt-3">
          Este aviso no bloquea la captura. Sirve para que Ventas avise oportunamente que se necesita existencia antes de surtir la salida.
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Entendido, continuar</button>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="newOrderConfirmModal" 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" id="newOrderConfirmTitle">Confirmar orden</div>
          <div class="muted small">Valida la información antes de guardar la orden para Almacén.</div>
        </div>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div id="newOrderConfirmBody" class="alert-soft"></div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">No, revisar</button>
        <button type="button" id="btnNewOrderConfirmYes" class="btn btn-green-soft">
          <i class="bi bi-check-circle me-1"></i>Sí, procesar
        </button>
      </div>
    </div>
  </div>
</div>

<!-- Modal resumen orden creada / actualizada -->
<div class="modal fade" id="newOrderSummaryModal" 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">Resumen de orden de salida</div>
          <div class="muted small">La orden quedó registrada y visible para el área 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 id="newOrderSummaryBody" class="alert-soft"></div>
      </div>
      <div class="modal-footer">
        <button type="button" id="btnNewOrderSummaryFinish" class="btn btn-primary-soft">
          <i class="bi bi-check2-circle me-1"></i>Finalizar
        </button>
      </div>
    </div>
  </div>
</div>

<!-- Modal confirmar / resultado salida -->
<div class="modal fade" id="dispatchModal" 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" id="dispatchTitle">Confirmar salida</div>
          <div class="muted small" id="dispatchSubtitle">Ventas generará la salida de almacén para el pedido seleccionado.</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="dispatchOrderId">
        <div id="dispatchBody" class="alert-soft"></div>
        <div class="mt-3">
          <label class="form-label">Notas de ventas para la salida</label>
          <input id="dispatchNotes" class="form-control" placeholder="Opcional">
        </div>
        <div id="dispatchWarnings" class="mt-3"></div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button>
        <button type="button" id="btnConfirmDispatch" class="btn btn-green-soft">
          <i class="bi bi-check-circle me-1"></i>Confirmar y generar salida
        </button>
        <a id="btnDispatchPdf" class="btn btn-primary-soft d-none" href="#" target="_blank" rel="noopener">
          <i class="bi bi-file-earmark-pdf me-1"></i>Descargar PDF
        </a>
      </div>
    </div>
  </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_MANAGE = <?= json_encode($canManage) ?>;
const CURRENT_LOCATION_ID = <?= json_encode($currentLocationId) ?>;
const IS_HIELO_BARRA_SCOPE = <?= json_encode($isHieloBarraScope) ?>;

const $ = id => document.getElementById(id);

function h(s){
  return String(s ?? '').replace(/[&<>"]/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));
}
function num(v){
  const n = parseFloat(v);
  return isNaN(n) ? 0 : n;
}
function money(v){ return num(v).toFixed(2); }
function qty(v){ return String(Math.round(num(v))); }
function qtyBar(v){ return String(Math.round(num(v))); }
function dt(v){
  if(!v) return '';
  const s = String(v).replace('T', ' ');
  return s.length >= 16 ? s.slice(0,16) : s;
}
function isBarSku(sku){ return String(sku || '').trim().toUpperCase() === 'BARRA 150 KILOS'; }
function statusClass(st){
  st = String(st || '').toUpperCase();
  if (st === 'PENDIENTE') return 'st-pendiente';
  if (st === 'EN_REVISION') return 'st-revision';
  if (st === 'AUTORIZADA') return 'st-autorizada';
  if (st === 'RECHAZADA') return 'st-rechazada';
  if (st === 'CANCELADA') return 'st-cancelada';
  if (st === 'SURTIENDO') return 'st-surtiendo';
  if (st === 'EN_RUTA') return 'st-en_ruta';
  if (st === 'ENTREGADA') return 'st-entregada';
  return 'st-pendiente';
}
function statusLabel(st){
  st = String(st || '').toUpperCase();
  const map = {
    'PENDIENTE': 'Pendiente',
    'EN_REVISION': 'En revisión',
    'AUTORIZADA': 'Autorizada',
    'RECHAZADA': 'Rechazada',
    'CANCELADA': 'Cancelada',
    'SURTIENDO': 'Surtiendo',
    'EN_RUTA': 'En ruta',
    'ENTREGADA': 'Entregada'
  };
  return map[st] || st;
}
function customerRoleLabel(v){
  return String(v || '').toLowerCase() === 'tienda' ? 'Tienda' : 'Cliente';
}

async function apiGet(action, params = {}){
  const u = new URL(location.href);
  u.search = '';
  u.searchParams.set('action', action);
  Object.entries(params).forEach(([k,v]) => {
    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 { return JSON.parse(txt); }
  catch(e){
    console.error(txt);
    return {ok:false,error:'NO_JSON'};
  }
}

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 { return JSON.parse(txt); }
  catch(e){
    console.error(txt);
    return {ok:false,error:'NO_JSON'};
  }
}

async function apiPostForm(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 { return JSON.parse(txt); }
  catch(e){ console.error(txt); return {ok:false,error:'NO_JSON'}; }
}

let detailModal;
let inventoryModal;
let customersModal;
let customerUsersModal;
let productsModal;
let customerEditModal;
let customerAddressesModal;
let dispatchModal;
let newOrderModal;
let newOrderConfirmModal;
let newOrderSummaryModal;
let newOrderCloseConfirmModal;
let pendingNewOrderPayload = null;
let allowNewOrderClose = false;
let newOrderStockRows = [];
let newOrderItems = [];
let newOrderPackagingRows = [];
let newOrderCustomerRows = [];
let newOrderRouteRows = [];
let newOrderRouteStockRows = [];
let botellaPalletValidationModal;
let newOrderNoStockModal;
let salesAiModal;
let currentRows = [];
let currentView = 'active';
let mailboxRows = [];
let mailboxFolder = 'all';
let mailboxSelectedIndex = -1;
let inventoryLocations = [];
let currentCustomerUsers = [];
let currentCustomerAccess = { customerId: 0, customerName: '' };
let customerTimer = null;
let productTimer = null;

async function loadLocationFilters(){
  const j = await apiGet('location_options');
  if(!j.ok){
    alert(j.error || 'No se pudieron cargar las ubicaciones');
    return;
  }

  inventoryLocations = j.rows || [];

  $('fLocation').innerHTML = '<option value="">Todas</option>' +
    inventoryLocations.map(r => `<option value="${r.id}">${h(r.name)}${r.code ? ' (' + h(r.code) + ')' : ''}</option>`).join('');

  $('invLocationId').innerHTML = inventoryLocations.length
    ? inventoryLocations.map(r => `<option value="${r.id}">${h(r.name)}${r.code ? ' (' + h(r.code) + ')' : ''}</option>`).join('')
    : `<option value="">Sin ubicaciones</option>`;

  if ($('fLocation') && CURRENT_LOCATION_ID > 0) $('fLocation').value = String(CURRENT_LOCATION_ID);
  if ($('invLocationId') && CURRENT_LOCATION_ID > 0) $('invLocationId').value = String(CURRENT_LOCATION_ID);
}

async function loadOrders(){
  const j = await apiGet('orders_mailbox', {
    date_from: $('fDateFrom').value,
    date_to: $('fDateTo').value,
    customer: $('fCustomer').value.trim(),
    location_id: $('fLocation').value
  });

  if(!j.ok){
    alert(j.error || 'Error al cargar pedidos');
    return;
  }

  mailboxRows = j.rows || [];
  currentRows = mailboxRows;

  const statuses = row => String(row.status || '').toUpperCase();
  $('mailCountAll').textContent = String(mailboxRows.length);
  $('mailCountReview').textContent = String(mailboxRows.filter(x => ['PENDIENTE','EN_REVISION'].includes(statuses(x))).length);
  $('mailCountAuthorized').textContent = String(mailboxRows.filter(x => statuses(x) === 'AUTORIZADA' && !Number(x.outbound_id || 0) && String(x.forklift_status || '').toUpperCase() !== 'COMPLETA').length);
  $('mailCountShipping').textContent = String(mailboxRows.filter(x => ['SURTIENDO','EN_RUTA'].includes(statuses(x)) || String(x.forklift_status || '').toUpperCase() === 'COMPLETA' || (Number(x.outbound_id || 0) > 0 && statuses(x) !== 'ENTREGADA')).length);
  $('mailCountDone').textContent = String(mailboxRows.filter(x => statuses(x) === 'ENTREGADA').length);
  $('mailCountCancelled').textContent = String(mailboxRows.filter(x => ['CANCELADA','RECHAZADA'].includes(statuses(x))).length);

  renderOrders();
  await loadSalesInsights();
}

function orderPdfUrl(row){
  const folio = row.sales_order_ref ? row.sales_order_ref : ('PED-' + row.id);
  let url = `orden_entrega_pdf.php?order_id=${encodeURIComponent(row.id)}&folio=${encodeURIComponent(folio)}`;
  if(Number(row.outbound_id || 0) > 0){
    url += `&outbound_id=${encodeURIComponent(row.outbound_id)}`;
  }
  return url;
}

function mailboxStatusInfo(rowOrStatus){
  const row = (rowOrStatus && typeof rowOrStatus === 'object') ? rowOrStatus : {status: rowOrStatus};
  const s = String(row.status || '').toUpperCase();
  const outboundId = Number(row.outbound_id || 0);
  const forkliftStatus = String(row.forklift_status || '').toUpperCase();

  // Mismo criterio visual que Almacén Botella:
  // 1) Si ya existe salida real, mostrar SALIDA GENERADA.
  // 2) Si Montacargas confirmó carga, mostrar LISTA PARA SALIR CON EL CLIENTE.
  // 3) Si no, mostrar el estatus comercial normal.
  if (s === 'ENTREGADA') return {cls:'done', label:'Entregada'};
  if (outboundId > 0) return {cls:'done', label:'Salida generada'};
  if (forkliftStatus === 'COMPLETA') return {cls:'pending', label:'Lista para salir con el cliente'};

  if (['PENDIENTE','EN_REVISION'].includes(s)) return {cls:'review', label:statusLabel(s)};
  if (s === 'AUTORIZADA') return {cls:'authorized', label:'Autorizada'};
  if (['SURTIENDO','EN_RUTA'].includes(s)) return {cls:'shipping', label:statusLabel(s)};
  if (s === 'ENTREGADA') return {cls:'done', label:'Entregada'};
  if (['CANCELADA','RECHAZADA'].includes(s)) return {cls:'cancelled', label:statusLabel(s)};
  return {cls:'pending', label:statusLabel(s)};
}



function cleanUserNotes(raw){
  let text = String(raw || '').replace(/\r/g,'').trim();
  if(!text) return '';
  const kept = [];
  text.split('\n').forEach(line => {
    let l = String(line || '').trim();
    if(!l) return;
    const upper = l.toUpperCase();
    if(upper.startsWith('ORDEN DE SALIDA BOTELLA CREADA DESDE VENTAS')) return;
    if(upper.startsWith('ORDEN CREADA DESDE VENTAS')) return;
    if(upper.startsWith('SALIDA GENERADA DESDE')) return;
    if(upper.startsWith('PEDIDO:')) return;
    if(upper.startsWith('CLIENTE:')) return;
    if(upper.startsWith('RUTA:')) return;
    if(upper.startsWith('RUTA DLX:')) return;
    if(upper.startsWith('UNIDAD A CARGAR:')) return;
    if(upper.startsWith('CHOFER:')) return;
    if(upper.startsWith('FECHA/HORA SALIDA:')) return;
    if(upper.startsWith('ADVERTENCIAS:')) return;
    const noteMatch = l.match(/Notas\s+(?:almac[eé]n|ventas|reparto)\s*:\s*(.*)$/i);
    if(noteMatch && noteMatch[1]) l = noteMatch[1].trim();
    if(l) kept.push(l);
  });
  if(kept.length) return kept.join('\n');
  return text
    .replace(/Orden de salida Botella creada desde Ventas\.?/ig,'')
    .replace(/Orden creada desde Ventas\.?/ig,'')
    .replace(/Ruta\s*:\s*[^\n]+/ig,'')
    .replace(/Ruta DLX\s*:\s*[^\n]+/ig,'')
    .replace(/Unidad a cargar\s*:\s*[^\n]+/ig,'')
    .replace(/Chofer\s*:\s*[^\n]+/ig,'')
    .trim();
}

function mailboxFilteredRows(){
  return mailboxRows.filter(r => {
    const s = String(r.status || '').toUpperCase();
    if (mailboxFolder === 'review') return ['PENDIENTE','EN_REVISION'].includes(s);
    if (mailboxFolder === 'authorized') return s === 'AUTORIZADA' && !Number(r.outbound_id || 0) && String(r.forklift_status || '').toUpperCase() !== 'COMPLETA';
    if (mailboxFolder === 'shipping') return ['SURTIENDO','EN_RUTA'].includes(s) || String(r.forklift_status || '').toUpperCase() === 'COMPLETA' || (Number(r.outbound_id || 0) > 0 && s !== 'ENTREGADA');
    if (mailboxFolder === 'done') return s === 'ENTREGADA';
    if (mailboxFolder === 'cancelled') return ['CANCELADA','RECHAZADA'].includes(s);
    return true;
  });
}

function renderOrders(){
  const rows = mailboxFilteredRows();
  const names = {
    all:'Todos los pedidos',
    review:'Pedidos por revisar',
    authorized:'Pedidos autorizados por surtir',
    shipping:'Pedidos en proceso',
    done:'Pedidos entregados',
    cancelled:'Pedidos cancelados / rechazados'
  };
  $('mailFolderTitle').textContent = names[mailboxFolder] || names.all;
  $('mailFolderSub').textContent = `${rows.length} registro(s) visibles`;

  if(!rows.length){
    $('mailItems').innerHTML = `<div class="mail-empty"><i class="bi bi-inbox fs-2 mb-2"></i><div>Sin pedidos en esta carpeta.</div></div>`;
    $('mailDetail').innerHTML = `<div class="mail-empty"><i class="bi bi-envelope-open fs-1 mb-2"></i><div class="fw-bold">Sin registros para mostrar</div></div>`;
    return;
  }

  $('mailItems').innerHTML = rows.map((r, idx) => {
    const folio = r.sales_order_ref || ('PED-' + r.id);
    const state = mailboxStatusInfo(r);
    const client = r.business_name || r.contact_name || 'Cliente';
    const route = r.route_no ? `Ruta ${r.route_no}` : 'Sin ruta';
    return `
      <button type="button" class="mail-item ${idx === 0 ? 'active' : ''}" data-mail-index="${idx}">
        <div class="d-flex justify-content-between gap-2 align-items-start">
          <div class="mail-folio mono">${h(folio)}</div>
          <span class="mail-status ${state.cls}">${h(state.label)}</span>
        </div>
        <div class="mail-sub">${h(client)}</div>
        <div class="mail-bottom">
          <span>${h(r.requested_delivery_date || '')} · ${h(route)}</span>
          <span class="mono fw-bold">${h(qty(r.total_qty || 0))} pz</span>
        </div>
      </button>
    `;
  }).join('');

  selectMailboxOrder(0);
}

async function selectMailboxOrder(index){
  const rows = mailboxFilteredRows();
  const r = rows[index];
  if(!r) return;
  mailboxSelectedIndex = index;
  document.querySelectorAll('#mailItems .mail-item').forEach((el,i) => el.classList.toggle('active', i === index));

  const detail = await apiGet('order_detail', {order_id:r.id});
  if(!detail.ok){
    $('mailDetail').innerHTML = `<div class="mail-empty">No fue posible consultar el detalle del pedido.</div>`;
    return;
  }
  const o = detail.order || r;
  const items = detail.items || [];
  const folio = o.sales_order_ref || ('PED-' + o.id);
  const state = mailboxStatusInfo(o);
  const locText = `${o.location_name || ''}${o.location_code ? ' (' + o.location_code + ')' : ''}`;
  const canDispatch = CAN_MANAGE && String(o.status || '').toUpperCase() === 'AUTORIZADA' && !Number(o.outbound_id || 0);
  const canEdit = CAN_MANAGE && String(o.source || '').toUpperCase() === 'VENTAS' && !Number(o.outbound_id || 0) && ['AUTORIZADA','PENDIENTE','EN_REVISION'].includes(String(o.status || '').toUpperCase());
  const hasPdf = Number(o.outbound_id || 0) > 0;
  const userNotes = cleanUserNotes(o.comments || '');

  const actionButtons = `
    <button class="btn toolbar-btn btn-sm" type="button" onclick="openDetail(${Number(o.id)})"><i class="bi bi-eye me-1"></i>Detalle completo</button>
    ${canEdit ? `<button class="btn toolbar-btn btn-sm" type="button" onclick="openEditWarehouseOrder(${Number(o.id)})"><i class="bi bi-pencil-square me-1"></i>Editar</button>` : ''}
    <!-- Ventas no realiza carga de camión ni salida física; Almacén continúa el flujo. -->
    ${hasPdf ? `<a class="btn btn-primary-soft btn-sm" href="${h(orderPdfUrl(o))}" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</a>` : ''}
  `;

  const actionMenu = `
    <div class="dropdown order-action-menu">
      <button class="btn btn-primary-soft btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
        <i class="bi bi-list me-1"></i>Acciones
      </button>
      <div class="dropdown-menu dropdown-menu-end">
        <button class="dropdown-item" type="button" onclick="openDetail(${Number(o.id)})"><i class="bi bi-eye me-2"></i>Detalle completo</button>
        ${canEdit ? `<button class="dropdown-item" type="button" onclick="openEditWarehouseOrder(${Number(o.id)})"><i class="bi bi-pencil-square me-2"></i>Editar</button>` : ''}
        <!-- Acciones de carga/salida física se realizan en Almacén. -->
        ${hasPdf ? `<a class="dropdown-item" href="${h(orderPdfUrl(o))}" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf me-2"></i>PDF</a>` : ''}
      </div>
    </div>
  `;

  const itemsHtml = items.length ? items.map(it => `
    <tr>
      <td class="mono fw-bold">${h(it.purchase_order || o.purchase_order || '—')}</td>
      <td>
        <div class="mono fw-bold">${h(it.sku || '')}</div>
        <div class="small muted">${h(it.product_name || '')}</div>
      </td>
      <td>
        <div class="fw-semibold">${h(it.packaging_name || '—')}</div>
        <div class="small muted">${h(it.pallet_type || '')}${it.pallet_specification ? ' · ' + h(it.pallet_specification) : ''}</div>
      </td>
      <td class="text-end mono fw-bold">${h(qty(it.pallets_qty || 0))}</td>
      <td class="text-end mono">${h(qty(it.packages_per_pallet || 0))}</td>
      <td class="text-end mono">${h(qty(it.pieces_per_package || 0))}</td>
      <td class="text-end mono">${h(qty(it.packages_qty || 0))}</td>
      <td class="text-end mono fw-bold">${h(qty(it.qty || 0))}</td>
    </tr>
  `).join('') : '<tr><td colspan="8" class="small muted text-center py-3">Sin artículos.</td></tr>';

  $('mailDetail').innerHTML = `
    <div class="d-flex justify-content-between gap-2 align-items-start flex-wrap mb-3">
      <div>
        <div class="small muted">Folio</div>
        <div class="fw-bold fs-5 mono">${h(folio)}</div>
        <div class="small muted">${h(o.business_name || o.contact_name || '')}</div>
      </div>
      <span class="mail-status ${state.cls}">${h(state.label)}</span>
    </div>
    <div class="row g-2 mb-3">
      <div class="col-6"><div class="mail-detail-card h-100"><div class="mail-detail-label">Entrega</div><div class="mail-detail-value mono">${h(o.requested_delivery_date || '—')} ${h(o.requested_delivery_time || '')}</div></div></div>
      <div class="col-6"><div class="mail-detail-card h-100"><div class="mail-detail-label">Planta</div><div class="mail-detail-value">${h(locText || '—')}</div></div></div>
      <div class="col-12"><div class="mail-detail-card"><div class="mail-detail-label">Dirección</div><div class="mail-detail-value">${h(o.delivery_address || '—')}</div></div></div>
    </div>
    ${userNotes ? `<div class="user-notes-card mb-3"><div class="mail-detail-label"><i class="bi bi-chat-left-text me-1"></i>Notas del usuario</div><div class="note-text mt-1">${h(userNotes)}</div></div>` : ''}
    <div class="table-wrap mb-3">
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead>
            <tr>
              <th>OC</th>
              <th>Artículo</th>
              <th>Embalaje</th>
              <th class="text-end">Tarimas</th>
              <th class="text-end">Paq/Tar</th>
              <th class="text-end">Pz/Paq</th>
              <th class="text-end">Paquetes</th>
              <th class="text-end">Botellas</th>
            </tr>
          </thead>
          <tbody>${itemsHtml}</tbody>
        </table>
      </div>
    </div>
    <div class="order-action-bar">
      <div>
        <div class="order-action-title">Acciones de la orden</div>
        <div class="small muted">En celular/tablet se agrupan en menú.</div>
      </div>
      <div class="order-action-buttons">${actionButtons}</div>
      ${actionMenu}
    </div>
  `;
}

async function loadSalesInsights(){
  const locationId = $('fLocation').value || String(CURRENT_LOCATION_ID);
  const j = await apiGet('sales_insights', {
    location_id: locationId,
    date_from: $('fDateFrom').value,
    date_to: $('fDateTo').value
  });
  if(!j.ok){
    $('aiSuggestions').innerHTML = `<div class="ai-alert critical"><div class="ai-alert-title">No se pudo generar el análisis</div>${h(j.error || '')}</div>`;
    if ($('aiFloatingRisks')) { $('aiFloatingRisks').textContent = '!'; $('aiFloatingRisks').classList.remove('is-zero'); }
    return;
  }

  const s = j.summary || {};
  $('aiRange').textContent = `· ${j.filters.date_from} a ${j.filters.date_to}`;
  $('aiOrders').textContent = String(s.orders_total || 0);
  $('aiDispatch').textContent = String(s.pending_dispatch || 0);
  $('aiReview').textContent = String(s.pending_review || 0);
  $('aiRisks').textContent = String(s.risks || 0);
  if ($('aiFloatingRisks')) {
    $('aiFloatingRisks').textContent = String(s.risks || 0);
    $('aiFloatingRisks').classList.toggle('is-zero', Number(s.risks || 0) === 0);
  }
  $('mailInventoryNote').textContent = j.inventory_note || 'Inventario surtible por almacenes de la planta activa.';

  $('aiSuggestions').innerHTML = (j.recommendations || []).map(x => `
    <div class="ai-alert ${h(x.level || 'info')}">
      <div class="ai-alert-title">${h(x.title || '')}</div>
      <div>${h(x.text || '')}</div>
    </div>
  `).join('');

  const risks = j.risks || [];
  $('aiRiskRows').innerHTML = risks.length ? risks.map(x => `
    <div class="ai-row">
      <div>
        <div class="ai-row-title">${h(x.sku || '')}</div>
        <div class="ai-row-sub">Solicitado ${h(qty(x.requested_qty || 0))} · disponible ${h(qty(x.available || 0))}</div>
      </div>
      <span class="ai-tag ${h(x.risk_level || 'warning')}">Faltan ${h(qty(x.shortage || 0))}</span>
    </div>
  `).join('') : '<div class="small muted">No hay faltantes en pedidos autorizados.</div>';

  const customers = j.top_customers || [];
  $('aiCustomers').innerHTML = customers.length ? customers.map(x => `
    <div class="ai-row">
      <div>
        <div class="ai-row-title">${h(x.business_name || x.contact_name || '')}</div>
        <div class="ai-row-sub">${Number(x.order_count || 0)} pedido(s)</div>
      </div>
      <span class="ai-tag">${h(qty(x.qty_requested || 0))} pz</span>
    </div>
  `).join('') : '<div class="small muted">Sin actividad en el periodo.</div>';
}

async function openDetail(orderId){
  const j = await apiGet('order_detail', {order_id: orderId});
  if(!j.ok){
    alert(j.error || 'No se pudo cargar el detalle');
    return;
  }

  const o = j.order || {};
  const items = j.items || [];
  const logs = j.logs || [];
  const folio = o.sales_order_ref ? o.sales_order_ref : ('PED-' + o.id);
  const locText = `${o.location_name || ''}${o.location_code ? ' (' + o.location_code + ')' : ''}`;
  const visualState = mailboxStatusInfo(o);

  $('mOrderId').value = String(o.id || '');
  $('mNewStatus').value = '';
  $('mComment').value = '';

  $('detailHeader').innerHTML = `
    <div class="col-12 col-md-4">
      <div class="log-item">
        <div class="small">Folio</div>
        <div class="fw-bold mono">${h(folio)}</div>
      </div>
    </div>
    <div class="col-12 col-md-4">
      <div class="log-item">
        <div class="small">Cliente</div>
        <div class="fw-bold">${h(o.business_name || '')}</div>
        <div class="small">${h(o.contact_name || '')}</div>
      </div>
    </div>
    <div class="col-12 col-md-4">
      <div class="log-item">
        <div class="small">Estatus</div>
        <div><span class="mail-status ${visualState.cls}">${h(visualState.label)}</span></div>
      </div>
    </div>

    <div class="col-12 col-md-4">
      <div class="log-item">
        <div class="small">Fecha solicitada</div>
        <div class="fw-semibold">${h(o.requested_delivery_date || '')} ${h(o.requested_delivery_time || '')}</div>
      </div>
    </div>
    <div class="col-12 col-md-4">
      <div class="log-item">
        <div class="small">Ruta</div>
        <div class="fw-semibold">${o.route_no ? 'Ruta ' + h(o.route_no) : '—'}</div>
      </div>
    </div>
    <div class="col-12 col-md-4">
      <div class="log-item">
        <div class="small">Dirección entrega</div>
        <div class="fw-semibold">${h(o.delivery_address || '')}</div>
      </div>
    </div>
    <div class="col-12 col-md-4">
      <div class="log-item">
        <div class="small">Total</div>
        <div class="fw-bold mono">${money(o.total)}</div>
      </div>
    </div>

    <div class="col-12 col-md-6">
      <div class="log-item">
        <div class="small">Planta / ubicación</div>
        <div class="fw-semibold">${h(locText || '—')}</div>
      </div>
    </div>
    <div class="col-12 col-md-6">
      <div class="log-item">
        <div class="small">Autorizado / rechazado</div>
        <div class="fw-semibold">
          Aprobó: ${h(o.approved_by_name || '—')} ${o.approved_at ? '(' + h(dt(o.approved_at)) + ')' : ''}
          <br>
          Rechazó: ${h(o.rejected_by_name || '—')} ${o.rejected_at ? '(' + h(dt(o.rejected_at)) + ')' : ''}
        </div>
      </div>
    </div>

    <div class="col-12">
      <div class="log-item">
        <div class="small">Comentarios del pedido</div>
        <div class="fw-semibold">${h(o.comments || '—')}</div>
      </div>
    </div>
  `;

  $('detailItems').innerHTML = items.map(it => `
    <tr>
      <td class="mono fw-bold">${h(it.purchase_order || o.purchase_order || '—')}</td>
      <td>
        <div class="fw-semibold mono">${h(it.sku || '')}</div>
        <div class="small muted">${h(it.product_name || '')}</div>
      </td>
      <td>
        <div class="fw-semibold">${h(it.packaging_name || '—')}</div>
        <div class="small muted">${h(it.pallet_type || '')}${it.pallet_specification ? ' · ' + h(it.pallet_specification) : ''}</div>
      </td>
      <td class="text-end mono fw-bold">${qty(it.pallets_qty || 0)}</td>
      <td class="text-end mono">${qty(it.packages_per_pallet || 0)}</td>
      <td class="text-end mono">${qty(it.pieces_per_package || 0)}</td>
      <td class="text-end mono">${qty(it.packages_qty || 0)}</td>
      <td class="text-end mono fw-bold">${qty(it.qty || 0)}</td>
    </tr>
  `).join('');

  $('detailLogs').innerHTML = logs.length
    ? logs.map(l => `
        <div class="log-item">
          <div class="d-flex justify-content-between flex-wrap gap-2">
            <div>
              <div class="fw-semibold">
                ${h(statusLabel(l.old_status || '—'))} → ${h(statusLabel(l.new_status || ''))}
              </div>
              <div class="small">
                ${h(l.changed_by_name || 'Sistema')} · ${h(l.changed_by_role || '')}
              </div>
              <div class="small">${h(l.comment || '')}</div>
            </div>
            <div class="small mono">${h(dt(l.created_at || ''))}</div>
          </div>
        </div>
      `).join('')
    : `<div class="small muted">Sin movimientos de bitácora.</div>`;

  detailModal.show();
}

async function saveStatus(){
  const orderId = parseInt($('mOrderId').value || '0', 10);
  const newStatus = $('mNewStatus').value;
  const comment = $('mComment').value.trim();

  if(orderId <= 0){
    alert('Pedido inválido');
    return;
  }
  if(!newStatus){
    alert('Selecciona un nuevo estatus');
    return;
  }

  const j = await apiPost('status_update', {
    order_id: orderId,
    new_status: newStatus,
    comment: comment
  });

  if(!j.ok){
    alert(j.error || 'No se pudo actualizar el estatus');
    return;
  }

  detailModal.hide();
  await loadOrders();
}


function switchOrderView(view){
  currentView = view;
  $('tabActive').classList.toggle('active', view === 'active');
  $('tabHistory').classList.toggle('active', view === 'history');
  loadOrders();
}


function todayIso(){
  const d = new Date();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${d.getFullYear()}-${m}-${day}`;
}

function newOrderSelectedProduct(){
  const pid = parseInt($('newOrderProduct').value || '0', 10);
  return newOrderStockRows.find(r => Number(r.product_id) === pid) || null;
}

function selectedPackaging(){
  const id = parseInt($('newOrderPackaging')?.value || '0', 10);
  return newOrderPackagingRows.find(r => Number(r.id || 0) === id) || null;
}

function showBotellaPalletValidation(message){
  $('botellaPalletValidationText').textContent = message;
  botellaPalletValidationModal.show();
}

function newOrderStockWarningKey(productId, mode, reason){
  return `${mode || newOrderType()}-${Number(productId || 0)}-${reason || 'NO_STOCK'}`;
}

function showNewOrderStockWarning(row, requestedQty = 0){
  if (!row) return;
  const available = Math.max(0, Math.floor(num(row.on_hand || 0)));
  const requested = Math.max(0, Math.round(num(requestedQty || 0)));
  const isRoute = newOrderIsRoute();
  const flowLabel = isRoute ? 'ruta de reparto' : 'salida a cliente';
  const routeLabel = $('newOrderRoute')?.selectedOptions?.[0]?.textContent?.trim() || 'ruta seleccionada';
  const sku = String(row.sku || 'Artículo');
  const name = String(row.name || '');
  const shortage = requested > available ? requested - available : (available <= 0 ? requested : 0);

  let text = `${sku}${name ? ' · ' + name : ''} no tiene existencia disponible suficiente en almacenes físicos de planta para esta ${flowLabel}.`;
  if (available <= 0) {
    text += ` Disponible actual: 0 botellas.`;
  } else {
    text += ` Disponible actual: ${available.toLocaleString('es-MX')} botellas.`;
  }
  if (requested > 0) {
    text += ` Cantidad solicitada: ${requested.toLocaleString('es-MX')} botellas.`;
  }
  if (shortage > 0) {
    text += ` Faltante estimado: ${shortage.toLocaleString('es-MX')} botellas.`;
  }
  text += isRoute
    ? ` Puedes generar la OSA para ${routeLabel}, pero avisa que se necesita existencia antes de surtir o cargar la ruta.`
    : ` Puedes generar la OSV, pero avisa que se necesita existencia antes de surtir la salida al cliente.`;

  if ($('newOrderNoStockText')) $('newOrderNoStockText').textContent = text;
  if (newOrderNoStockModal) newOrderNoStockModal.show();
}

function warnIfSelectedProductHasNoStock(){
  const r = newOrderSelectedProduct();
  if (!r) return;
  const available = Math.floor(num(r.on_hand || 0));
  if (available > 0) return;
  const key = newOrderStockWarningKey(r.product_id, newOrderType(), 'NO_STOCK_SELECT');
  if (newOrderStockWarningShown[key]) return;
  newOrderStockWarningShown[key] = true;
  showNewOrderStockWarning(r, 0);
}

async function loadNewOrderCustomers(){
  const locationId = parseInt($('newOrderLocation').value || '0', 10);
  const j = await apiGet('customers', {location_id: locationId});
  newOrderCustomerRows = j.ok ? (j.rows || []).filter(c => Number(c.active) === 1) : [];
  $('newOrderCustomerId').innerHTML = '<option value="">Selecciona cliente registrado</option>' +
    newOrderCustomerRows.map(c => `<option value="${Number(c.id)}">${h((c.customer_code ? c.customer_code + ' · ' : '') + (c.business_name || ''))}</option>`).join('');
  fillNewOrderItemCustomerOptions();
  await loadNewOrderCustomerAddresses();
  updateNewOrderOcRelationUi();
}

async function loadNewOrderCustomerAddresses(selectedAddressId=''){
  const customerId = parseInt($('newOrderCustomerId')?.value || '0', 10);
  const addressSelect = $('newOrderAddressId');
  if (!addressSelect) return;

  addressSelect.innerHTML = '<option value="">Selecciona punto de entrega</option>';
  if (!customerId) {
    $('newOrderAddress').value = '';
    return;
  }

  const j = await apiGet('customer_addresses', {customer_id: customerId});
  const rows = j.ok ? (j.rows || []).filter(a => Number(a.active) === 1) : [];
  addressSelect.innerHTML = '<option value="">Selecciona punto de entrega</option>' + rows.map(a => {
    const loc = a.location_name ? ' · ' + a.location_name : '';
    const label = (a.label || 'Punto de entrega') + loc;
    return `<option value="${Number(a.id)}" data-address="${h(a.address || '')}">${h(label)}</option>`;
  }).join('');

  if (selectedAddressId) {
    addressSelect.value = String(selectedAddressId);
  } else {
    const def = rows.find(a => Number(a.is_default) === 1) || rows[0];
    if (def) addressSelect.value = String(def.id);
  }

  syncNewOrderAddressFromSelect();
}

function syncNewOrderAddressFromSelect(){
  const opt = $('newOrderAddressId')?.selectedOptions?.[0];
  if (opt && opt.value) {
    $('newOrderAddress').value = opt.getAttribute('data-address') || '';
  }
}

function fillNewOrderItemCustomerOptions(){
  const sel = $('newOrderItemCustomerId');
  if (!sel) return;
  sel.innerHTML = '<option value="">Selecciona cliente</option>' +
    (newOrderCustomerRows || []).map(c => `<option value="${Number(c.id)}">${h((c.customer_code ? c.customer_code + ' · ' : '') + (c.business_name || ''))}</option>`).join('');
}
async function loadNewOrderItemAddresses(selectedAddressId=''){
  const customerId = parseInt($('newOrderItemCustomerId')?.value || '0', 10);
  const addressSelect = $('newOrderItemAddressId');
  if (!addressSelect) return;
  addressSelect.innerHTML = '<option value="">Selecciona punto</option>';
  if (!customerId) { if ($('newOrderItemAddress')) $('newOrderItemAddress').value=''; return; }
  const j = await apiGet('customer_addresses', {customer_id: customerId});
  const rows = j.ok ? (j.rows || []).filter(a => Number(a.active) === 1) : [];
  addressSelect.innerHTML = '<option value="">Selecciona punto</option>' + rows.map(a => {
    const loc = a.location_name ? ' · ' + a.location_name : '';
    const label = (a.label || 'Punto de entrega') + loc;
    return `<option value="${Number(a.id)}" data-address="${h(a.address || '')}">${h(label)}</option>`;
  }).join('');
  if (selectedAddressId) addressSelect.value = String(selectedAddressId);
  else {
    const def = rows.find(a => Number(a.is_default) === 1) || rows[0];
    if (def) addressSelect.value = String(def.id);
  }
  syncNewOrderItemAddressFromSelect();
}
function syncNewOrderItemAddressFromSelect(){
  const opt = $('newOrderItemAddressId')?.selectedOptions?.[0];
  if ($('newOrderItemAddress')) $('newOrderItemAddress').value = opt && opt.value ? (opt.getAttribute('data-address') || '') : '';
}
function updateNewOrderOcRelationUi(){
  // La relación por cambio de OC ya no se muestra como campo en pantalla.
  // Se decide mediante modal cuando el usuario captura una OC nueva.
  document.querySelectorAll('.order-client-item-dest-field').forEach(el=>el.classList.add('d-none'));
}

function normalizeOcValue(v){
  return String(v || '').trim().toUpperCase();
}

function headerOcContext(){
  return {
    relation: 'MISMO_CLIENTE_MISMA_DIRECCION',
    relation_label: 'Mismo cliente / misma dirección',
    customer_id: Number($('newOrderCustomerId')?.value || 0),
    address_id: Number($('newOrderAddressId')?.value || 0),
    customer_label: String($('newOrderCustomerId')?.selectedOptions?.[0]?.textContent || ''),
    delivery_address: String($('newOrderAddress')?.value || '')
  };
}

function ocRelationLabels(){
  return {
    MISMO_CLIENTE_MISMA_DIRECCION: 'Mismo cliente / misma dirección',
    MISMO_CLIENTE_OTRA_DIRECCION: 'Mismo cliente / diferente punto de entrega',
    OTRO_CLIENTE: 'Cliente diferente'
  };
}

function ensureOcContextStore(){
  window.newOrderOcContexts = window.newOrderOcContexts || {};
  return window.newOrderOcContexts;
}

function applyOcContextToHiddenFields(ctx){
  ctx = ctx || headerOcContext();
  if ($('newOrderOcRelation')) $('newOrderOcRelation').value = ctx.relation || 'MISMO_CLIENTE_MISMA_DIRECCION';
  if ($('newOrderItemCustomerId')) {
    $('newOrderItemCustomerId').disabled = false;
    fillNewOrderItemCustomerOptions();
    $('newOrderItemCustomerId').value = ctx.customer_id ? String(ctx.customer_id) : '';
  }
  if ($('newOrderItemAddressId')) {
    $('newOrderItemAddressId').innerHTML = ctx.address_id ? `<option value="${Number(ctx.address_id)}" data-address="${h(ctx.delivery_address || '')}" selected>Seleccionado</option>` : '<option value="">Selecciona punto</option>';
  }
  if ($('newOrderItemAddress')) $('newOrderItemAddress').value = ctx.delivery_address || '';
}

function getContextForOc(oc){
  const key = normalizeOcValue(oc);
  const store = ensureOcContextStore();
  if (store[key]) return store[key];
  const existing = (newOrderItems || []).find(x => normalizeOcValue(x.item_purchase_order) === key);
  if (existing) {
    return {
      relation: existing.oc_relation || 'MISMO_CLIENTE_MISMA_DIRECCION',
      relation_label: existing.oc_relation_label || (ocRelationLabels()[existing.oc_relation] || ''),
      customer_id: Number(existing.item_customer_id || 0),
      address_id: Number(existing.item_address_id || 0),
      customer_label: String(existing.customer_label || ''),
      delivery_address: String(existing.item_delivery_address || ''),
      oc_file_index: (existing.oc_file_index === null || existing.oc_file_index === undefined) ? null : existing.oc_file_index,
      oc_attachment_missing_reason: String(existing.oc_attachment_missing_reason || ''),
      oc_document_registered: true
    };
  }
  return null;
}

function shouldPromptForNewOc(oc){
  if (newOrderIsRoute()) return false;
  const key = normalizeOcValue(oc);
  if (!key) return false;
  if (getContextForOc(key)) return false;
  const uniqueExisting = new Set((newOrderItems || []).map(x => normalizeOcValue(x.item_purchase_order)).filter(Boolean));
  return uniqueExisting.size > 0;
}

function isOcDocumentAlreadyCaptured(oc){
  const key = normalizeOcValue(oc);
  if (!key) return false;
  const storeCtx = ensureOcContextStore()[key];
  if (storeCtx && storeCtx.oc_document_registered) return true;
  return (newOrderItems || []).some(x => normalizeOcValue(x.item_purchase_order) === key);
}

function updateOcAttachmentFieldsVisibility(){
  const isRoute = newOrderIsRoute();
  const oc = ($('newOrderItemPurchaseOrder')?.value || '').trim();
  const alreadyCaptured = !isRoute && oc !== '' && isOcDocumentAlreadyCaptured(oc);
  document.querySelectorAll('.order-client-item-oc-attachment-field').forEach(el => {
    el.classList.toggle('d-none', isRoute || alreadyCaptured);
  });
  document.querySelectorAll('.order-client-item-oc-reuse-hint').forEach(el => {
    el.classList.toggle('d-none', isRoute || !alreadyCaptured);
  });
  if (alreadyCaptured) {
    const ctx = getContextForOc(oc) || ensureOcContextStore()[normalizeOcValue(oc)] || {};
    const msg = ctx.oc_file_index !== null && ctx.oc_file_index !== undefined
      ? 'Esta OC ya tiene archivo adjunto registrado. No es necesario volver a cargarlo para otra presentación.'
      : (ctx.oc_attachment_missing_reason
        ? `Esta OC ya fue registrada sin adjunto. Motivo: ${h(ctx.oc_attachment_missing_reason)}.`
        : 'Esta OC ya fue registrada. No es necesario volver a adjuntar archivo para otra presentación.');
    if ($('newOrderOcReuseHint')) $('newOrderOcReuseHint').innerHTML = `<i class="bi bi-check-circle me-1"></i>${msg}`;
    if ($('newOrderItemOcFile')) $('newOrderItemOcFile').value = '';
    if ($('newOrderItemOcMissingReason')) $('newOrderItemOcMissingReason').value = '';
  }
}

async function loadOcModalAddresses(customerId, selectedAddressId=''){
  const sel = $('ocChangeAddressId');
  if (!sel) return;
  sel.innerHTML = '<option value="">Selecciona punto</option>';
  if (!customerId) { if ($('ocChangeAddressText')) $('ocChangeAddressText').value = ''; return; }
  const j = await apiGet('customer_addresses', {customer_id: customerId});
  const rows = j.ok ? (j.rows || []).filter(a => Number(a.active) === 1) : [];
  sel.innerHTML = '<option value="">Selecciona punto</option>' + rows.map(a => {
    const loc = a.location_name ? ' · ' + a.location_name : '';
    const label = (a.label || 'Punto de entrega') + loc;
    return `<option value="${Number(a.id)}" data-address="${h(a.address || '')}">${h(label)}</option>`;
  }).join('');
  if (selectedAddressId) sel.value = String(selectedAddressId);
  else {
    const def = rows.find(a => Number(a.is_default) === 1) || rows[0];
    if (def) sel.value = String(def.id);
  }
  syncOcModalAddressText();
}

function syncOcModalAddressText(){
  const opt = $('ocChangeAddressId')?.selectedOptions?.[0];
  if ($('ocChangeAddressText')) $('ocChangeAddressText').value = opt && opt.value ? (opt.getAttribute('data-address') || '') : '';
}

function updateOcChangeModalMode(){
  const rel = $('ocChangeRelation')?.value || 'MISMO_CLIENTE_MISMA_DIRECCION';
  const needsDest = rel === 'MISMO_CLIENTE_OTRA_DIRECCION' || rel === 'OTRO_CLIENTE';
  document.querySelectorAll('.oc-change-dest').forEach(el=>el.classList.toggle('d-none', !needsDest));
  const customerSel = $('ocChangeCustomerId');
  if (!customerSel) return;
  customerSel.disabled = rel === 'MISMO_CLIENTE_OTRA_DIRECCION';
  if (needsDest) {
    if (rel === 'MISMO_CLIENTE_OTRA_DIRECCION') {
      customerSel.value = $('newOrderCustomerId')?.value || '';
      loadOcModalAddresses(customerSel.value);
    } else if (!customerSel.value) {
      if ($('ocChangeAddressId')) $('ocChangeAddressId').innerHTML = '<option value="">Selecciona punto</option>';
      if ($('ocChangeAddressText')) $('ocChangeAddressText').value = '';
    }
  }
}

function ensureOcChangeModal(){
  if ($('ocChangeModal')) return;
  const div = document.createElement('div');
  div.innerHTML = `<div class="modal fade" id="ocChangeModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
      <div class="modal-content rounded-4">
        <div class="modal-header bg-primary text-white">
          <div>
            <h5 class="modal-title mb-0">Nueva OC detectada</h5>
            <div class="small opacity-75">Indica cómo se relaciona esta OC con el embarque actual.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>
        </div>
        <div class="modal-body">
          <div class="alert alert-info mb-3">OC capturada: <b class="mono" id="ocChangeNumber">—</b></div>
          <div class="row g-3">
            <div class="col-12">
              <label class="form-label">¿La nueva OC es para?</label>
              <select id="ocChangeRelation" class="form-select">
                <option value="MISMO_CLIENTE_MISMA_DIRECCION">Mismo cliente / mismo punto de entrega</option>
                <option value="MISMO_CLIENTE_OTRA_DIRECCION">Mismo cliente / diferente punto de entrega</option>
                <option value="OTRO_CLIENTE">Cliente diferente</option>
              </select>
            </div>
            <div class="col-12 col-md-6 oc-change-dest d-none">
              <label class="form-label">Cliente</label>
              <select id="ocChangeCustomerId" class="form-select"></select>
            </div>
            <div class="col-12 col-md-6 oc-change-dest d-none">
              <label class="form-label">Punto de entrega</label>
              <select id="ocChangeAddressId" class="form-select"></select>
            </div>
            <div class="col-12 oc-change-dest d-none">
              <label class="form-label">Dirección seleccionada</label>
              <input id="ocChangeAddressText" class="form-control" readonly>
            </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="btnOcChangeApply"><i class="bi bi-check-circle me-1"></i>Aplicar a esta OC</button>
        </div>
      </div>
    </div>
  </div>`;
  document.body.appendChild(div.firstElementChild);
  $('ocChangeRelation').addEventListener('change', updateOcChangeModalMode);
  $('ocChangeCustomerId').addEventListener('change', () => loadOcModalAddresses($('ocChangeCustomerId').value));
  $('ocChangeAddressId').addEventListener('change', syncOcModalAddressText);
}

function promptOcChangeContext(oc){
  return new Promise(resolve => {
    ensureOcChangeModal();
    const modalEl = $('ocChangeModal');
    const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
    $('ocChangeNumber').textContent = oc || '—';
    $('ocChangeRelation').value = 'MISMO_CLIENTE_MISMA_DIRECCION';
    const customerSel = $('ocChangeCustomerId');
    customerSel.innerHTML = '<option value="">Selecciona cliente</option>' +
      (newOrderCustomerRows || []).map(c => `<option value="${Number(c.id)}">${h((c.customer_code ? c.customer_code + ' · ' : '') + (c.business_name || ''))}</option>`).join('');
    customerSel.value = $('newOrderCustomerId')?.value || '';
    if ($('ocChangeAddressId')) $('ocChangeAddressId').innerHTML = '<option value="">Selecciona punto</option>';
    if ($('ocChangeAddressText')) $('ocChangeAddressText').value = '';
    updateOcChangeModalMode();

    const cleanup = () => {
      $('btnOcChangeApply').onclick = null;
      modalEl.removeEventListener('hidden.bs.modal', onHidden);
    };
    const onHidden = () => { cleanup(); resolve(null); };
    modalEl.addEventListener('hidden.bs.modal', onHidden, {once:true});
    $('btnOcChangeApply').onclick = () => {
      const rel = $('ocChangeRelation').value || 'MISMO_CLIENTE_MISMA_DIRECCION';
      let ctx = headerOcContext();
      ctx.relation = rel;
      ctx.relation_label = ocRelationLabels()[rel] || '';
      if (rel === 'MISMO_CLIENTE_OTRA_DIRECCION' || rel === 'OTRO_CLIENTE') {
        const cid = Number($('ocChangeCustomerId').value || 0);
        const aid = Number($('ocChangeAddressId').value || 0);
        if (!cid || !aid) {
          $('newOrderResult').innerHTML = '<div class="alert alert-danger">Selecciona cliente y punto de entrega para la nueva OC.</div>';
          return;
        }
        ctx.customer_id = cid;
        ctx.address_id = aid;
        ctx.customer_label = String($('ocChangeCustomerId').selectedOptions?.[0]?.textContent || '');
        ctx.delivery_address = String($('ocChangeAddressText').value || '');
      }
      ensureOcContextStore()[normalizeOcValue(oc)] = ctx;
      applyOcContextToHiddenFields(ctx);
      cleanup();
      modalEl.removeEventListener('hidden.bs.modal', onHidden);
      modal.hide();
      resolve(ctx);
    };
    modal.show();
  });
}

async function handleNewOrderItemOcChange(){
  if (newOrderIsRoute()) { updateOcAttachmentFieldsVisibility(); return; }
  const oc = ($('newOrderItemPurchaseOrder')?.value || '').trim();
  if (!oc) { updateOcAttachmentFieldsVisibility(); return; }
  const existing = getContextForOc(oc);
  if (existing) {
    applyOcContextToHiddenFields(existing);
    updateOcAttachmentFieldsVisibility();
    return;
  }
  if (shouldPromptForNewOc(oc)) {
    await promptOcChangeContext(oc);
  } else {
    const ctx = headerOcContext();
    ensureOcContextStore()[normalizeOcValue(oc)] = ctx;
    applyOcContextToHiddenFields(ctx);
  }
  updateOcAttachmentFieldsVisibility();
}


function newOrderType(){
  return String($('newOrderType')?.value || 'CLIENTE').toUpperCase();
}
function newOrderIsRoute(){ return newOrderType()==='RUTA'; }
function setNewOrderVehicleOptionsForMode(){
  const sel = $('newOrderVehicleUnit');
  if (!sel) return;
  if (newOrderIsRoute()) {
    sel.innerHTML = '<option value="Unidad de reparto a granel">Unidad de reparto a granel</option>';
    sel.value = 'Unidad de reparto a granel';
    sel.disabled = true;
  } else {
    sel.disabled = false;
    const current = String(sel.value || 'Caja seca');
    sel.innerHTML = '<option value="Caja seca">Caja seca</option><option value="Torton">Torton</option>';
    sel.value = current.toUpperCase().includes('TORTON') ? 'Torton' : 'Caja seca';
  }
}
function updateNewOrderMode(){
  const isRoute = newOrderIsRoute();
  document.querySelectorAll('.order-client-field').forEach(el=>el.classList.toggle('d-none', isRoute));
  document.querySelectorAll('.header-oc-field').forEach(el=>el.classList.add('d-none'));
  document.querySelectorAll('.order-client-item-oc-field').forEach(el=>el.classList.toggle('d-none', isRoute));
  document.querySelectorAll('.order-route-field').forEach(el=>el.classList.toggle('d-none', !isRoute));
  $('btnNewOrderSave').innerHTML = isRoute ? '<i class="bi bi-check-circle me-1"></i>Crear orden ruta' : '<i class="bi bi-check-circle me-1"></i>Crear orden';
  setNewOrderVehicleOptionsForMode();
  updateVehicleCapacity();
  if (isRoute) {
    $('newOrderCustomerId').value='';
    if ($('newOrderAddressId')) $('newOrderAddressId').innerHTML='<option value="">No aplica</option>';
    $('newOrderAddress').value='';
    $('newOrderPurchaseOrder').value='';
    if ($('newOrderOcFile')) $('newOrderOcFile').value='';
    $('newOrderItemPurchaseOrder').value='';
  } else {
    // En OSV la OC se captura por renglón para permitir cambio de OC dentro del mismo embarque.
    $('newOrderItemPurchaseOrder').value = $('newOrderPurchaseOrder')?.value || '';
  }
  updateNewOrderOcRelationUi();
  updateOcAttachmentFieldsVisibility();
  syncRouteLabel();
  loadNewOrderRouteStock();
  updateNewOrderStockInfo();
}
function vehicleCapacity(){
  if (newOrderIsRoute()) return Number.POSITIVE_INFINITY;
  const v = String($('newOrderVehicleUnit')?.value || 'Caja seca').toUpperCase();
  return v.includes('TORTON') ? 18 : 30;
}
function updateVehicleCapacity(){
  const cap = vehicleCapacity();
  if ($('newOrderVehicleCapacity')) {
    $('newOrderVehicleCapacity').value = newOrderIsRoute() ? 'Unidad de reparto a granel · sin límite de tarimas' : (cap + ' tarimas');
  }
  updateNewOrderCapacityBadge();
}
function totalNewOrderPallets(){
  return newOrderItems.reduce((a,it)=>a+Number(it.pallets_qty||0),0);
}
function updateNewOrderCapacityBadge(){
  const cap = vehicleCapacity();
  const used = totalNewOrderPallets();
  const badge = $('newOrderCapacityBadge');
  if (!badge) return;
  if (newOrderIsRoute()) {
    badge.textContent = used + ' tarima(s) · unidad de reparto a granel';
    badge.className = 'badge rounded-pill border bg-info-subtle text-info border-info';
    return;
  }
  badge.textContent = used + ' / ' + cap + ' tarimas';
  badge.className = 'badge rounded-pill border ' + (used>cap ? 'bg-danger-subtle text-danger border-danger' : (used===cap ? 'bg-success-subtle text-success border-success' : 'bg-light text-dark'));
}
function syncRouteLabel(){
  const opt = $('newOrderRoute')?.selectedOptions?.[0];
  if ($('newOrderRouteLabel')) $('newOrderRouteLabel').value = opt && opt.value ? opt.textContent.trim() : 'Selecciona una ruta';
}

function routeStockQty(productId){
  const row = (newOrderRouteStockRows || []).find(x => Number(x.product_id) === Number(productId));
  return row ? num(row.qty || 0) : 0;
}

function routeLoadQty(productId){
  return newOrderItems
    .filter(x => Number(x.product_id) === Number(productId))
    .reduce((a,x) => a + num(x.qty || 0), 0);
}

function routeProjectedQty(productId){
  return routeStockQty(productId) + routeLoadQty(productId);
}

function renderNewOrderRouteStock(){
  const box = $('newOrderRouteStockBox');
  if (!box) return;
  const routeNo = parseInt($('newOrderRoute')?.value || '0', 10);
  const code = $('newOrderRouteWarehouseCode');
  if (code) code.textContent = routeNo > 0 ? ('Almacén móvil RUTA-' + String(routeNo).padStart(2,'0')) : 'Selecciona ruta';
  if (!newOrderIsRoute()) {
    box.innerHTML = '<div class="route-stock-empty">No aplica para salida a cliente.</div>';
    return;
  }
  if (routeNo <= 0) {
    box.innerHTML = '<div class="route-stock-empty">Selecciona una ruta para consultar saldo.</div>';
    return;
  }
  if (!newOrderRouteStockRows.length) {
    box.innerHTML = '<div class="route-stock-empty">La ruta no tiene saldo actual registrado. La carga se agregará como nueva entrada al almacén móvil.</div>';
    return;
  }
  box.innerHTML = `
    <div class="table-responsive">
      <table class="table table-sm align-middle mb-0">
        <thead><tr><th>Artículo</th><th class="text-end">Saldo actual</th><th class="text-end">Nueva carga</th><th class="text-end">Proyectado</th></tr></thead>
        <tbody>
          ${newOrderRouteStockRows.map(r => {
            const load = routeLoadQty(r.product_id);
            const projected = routeStockQty(r.product_id) + load;
            return `<tr>
              <td><div class="mono fw-bold">${h(r.sku || '')}</div><div class="small muted">${h(r.name || '')}</div></td>
              <td class="text-end mono fw-bold">${h(qty(r.qty || 0))}</td>
              <td class="text-end mono">${h(qty(load))}</td>
              <td class="text-end mono fw-bold">${h(qty(projected))}</td>
            </tr>`;
          }).join('')}
        </tbody>
      </table>
    </div>`;
}

async function loadNewOrderRouteStock(){
  if (!newOrderIsRoute()) {
    newOrderRouteStockRows = [];
    renderNewOrderRouteStock();
    return;
  }
  const locationId = parseInt($('newOrderLocation')?.value || '0', 10);
  const routeNo = parseInt($('newOrderRoute')?.value || '0', 10);
  if (locationId <= 0 || routeNo <= 0) {
    newOrderRouteStockRows = [];
    renderNewOrderRouteStock();
    return;
  }
  if ($('newOrderRouteStockBox')) $('newOrderRouteStockBox').innerHTML = '<div class="route-stock-empty">Consultando saldo de ruta...</div>';
  const j = await apiGet('botella_route_stock', {location_id: locationId, route_no: routeNo});
  newOrderRouteStockRows = j.ok ? (j.rows || []) : [];
  renderNewOrderRouteStock();
  updateNewOrderStockInfo();
}
async function loadNewOrderRoutes(){
  const locationId = parseInt($('newOrderLocation').value || '0', 10);
  const j = await apiGet('botella_routes', {location_id: locationId});
  newOrderRouteRows = j.ok ? (j.rows || []) : [];
  $('newOrderRoute').innerHTML = '<option value="">Selecciona ruta configurada</option>' +
    newOrderRouteRows.map(r => `<option value="${Number(r.route_no)}">Ruta ${Number(r.route_no)} · ${h((r.route_code || '') + ' · ' + (r.route_name || ''))}</option>`).join('');
  syncRouteLabel();
  await loadNewOrderRouteStock();
}

async function loadNewOrderPackaging(){
  const r = newOrderSelectedProduct();
  newOrderPackagingRows = [];
  if (!r) {
    $('newOrderPackaging').innerHTML = '<option value="">Selecciona artículo</option>';
    return;
  }
  const j = await apiGet('botella_packaging_options', {product_id: Number(r.product_id)});
  newOrderPackagingRows = j.ok ? (j.rows || []) : [];
  $('newOrderPackaging').innerHTML = newOrderPackagingRows.length
    ? newOrderPackagingRows.map(p => `<option value="${Number(p.id || 0)}">${h(p.packaging_name || p.packaging_type)} · ${p.packages_per_pallet} paq/tar · ${p.pieces_per_package} pz/paq${Number(p.legacy_fallback||0)===1 ? ' · principal' : ''}</option>`).join('')
    : '<option value="">Sin embalaje configurado en Admin</option>';
  updateNewOrderStockInfo();
}

function renderNewOrderProducts(){
  const activeRows = newOrderStockRows
    .filter(r => Number(r.active) === 1)
    .sort((a,b) => String(a.sku || '').localeCompare(String(b.sku || '')));

  $('newOrderProduct').innerHTML = activeRows.length
    ? activeRows.map(r => `<option value="${Number(r.product_id)}">${h(r.sku)} · ${h(r.name)} · Disp. ${qty(r.on_hand)} botellas</option>`).join('')
    : '<option value="">Sin artículos activos</option>';

  loadNewOrderPackaging();
}

function updateNewOrderStockInfo(){
  const r = newOrderSelectedProduct();
  const p = selectedPackaging();
  if (!r) {
    $('newOrderStockInfo').innerHTML = 'Selecciona una ubicación para cargar artículos disponibles.';
    return;
  }
  const packagingText = p
    ? ` · Embalaje: <strong>${h(p.packages_per_pallet)}</strong> paquetes/tarima × <strong>${h(p.pieces_per_package)}</strong> botellas/paquete`
    : ' · Configura un embalaje en Admin';
  const routeText = newOrderIsRoute()
    ? ` · En ruta: <strong>${h(qty(routeStockQty(r.product_id)))}</strong> · Proyectado: <strong>${h(qty(routeProjectedQty(r.product_id)))}</strong>`
    : '';
  const available = Math.floor(num(r.on_hand || 0));
  const stockClass = available <= 0 ? 'text-danger' : '';
  const warningText = available <= 0
    ? ' <span class="badge rounded-pill text-bg-warning ms-1"><i class="bi bi-exclamation-triangle me-1"></i>Sin existencia disponible</span>'
    : '';
  $('newOrderStockInfo').innerHTML = `Disponible planta: <strong class="${stockClass}">${h(qty(r.on_hand))}</strong> botellas · ${h(r.sku || '')} ${h(r.name || '')}${warningText}${routeText}${packagingText}`;
  updateNewOrderCalcPreview();
}

async function loadNewOrderStock(){
  const locationId = parseInt($('newOrderLocation').value || '0', 10);
  if (locationId <= 0) {
    newOrderStockRows = [];
    renderNewOrderProducts();
    return;
  }
  $('newOrderStockInfo').innerHTML = 'Consultando existencia...';
  const j = await apiGet('inventory_by_location', {location_id: locationId});
  if (!j.ok) {
    newOrderStockRows = [];
    renderNewOrderProducts();
    $('newOrderStockInfo').innerHTML = `<span class="text-danger">${h(j.error || 'No se pudo consultar inventario')}</span>`;
    return;
  }
  newOrderStockRows = Array.isArray(j.rows) ? j.rows : [];
  renderNewOrderProducts();
}

function renderNewOrderItems(){
  const body = $('newOrderItemsBody');
  if (!newOrderItems.length) {
    body.innerHTML = '<tr><td colspan="10" class="text-center muted py-3">Agrega artículos por número de tarimas.</td></tr>';
    $('newOrderItemsHint').textContent = 'Agrega al menos un artículo con su embalaje para guardar la orden.';
    return;
  }
  body.innerHTML = newOrderItems.map((it, idx) => {
    const saldoRuta = newOrderIsRoute() ? routeStockQty(it.product_id) : null;
    const totalRuta = newOrderIsRoute() ? routeProjectedQty(it.product_id) : null;
    return `
    <tr>
      <td><div class="mono fw-bold">${h(it.item_purchase_order || '—')}</div><div class="small muted">${h(it.oc_relation_label || '')}</div>${it.oc_file_index!==null&&it.oc_file_index!==undefined?'<div class="small text-success"><i class="bi bi-paperclip"></i> OC adjunta</div>':(it.oc_attachment_missing_reason?`<div class="small text-warning">Sin adjunto: ${h(it.oc_attachment_missing_reason)}</div>`:'')}</td>
      <td><div class="mono fw-bold">${h(it.sku)}</div><div class="small muted">${h(it.name)}</div>${it.insufficient_stock ? `<div class="small text-warning fw-bold"><i class="bi bi-exclamation-triangle me-1"></i>Requiere existencia: faltan ${h(qty(it.stock_shortage || 0))} botellas</div>` : ''}</td>
      <td><div>${h(it.packaging_name)}</div><div class="small muted">${h(it.pallet_type || '')} ${h(it.pallet_specification || '')}</div></td>
      <td class="text-end mono">${newOrderIsRoute() ? h(qty(saldoRuta)) : '—'}</td>
      <td class="text-end mono fw-bold">${h(it.pallets_qty)}</td>
      <td class="text-end mono">${h(it.packages_per_pallet)}</td>
      <td class="text-end mono fw-bold">${h(it.packages_qty)}</td>
      <td class="text-end mono fw-bold">${h(it.qty)}</td>
      <td class="text-end mono fw-bold">${newOrderIsRoute() ? h(qty(totalRuta)) : '—'}</td>
      <td class="text-end"><button type="button" class="btn btn-danger-soft btn-sm" onclick="removeNewOrderItem(${idx})"><i class="bi bi-trash"></i></button></td>
    </tr>`;
  }).join('');
  $('newOrderItemsHint').textContent = `${newOrderItems.length} renglón(es). Total: ${totalNewOrderPallets()} tarima(s).`;
  updateNewOrderCapacityBadge();
}

function removeNewOrderItem(idx){
  newOrderItems.splice(idx, 1);
  renderNewOrderItems();
  renderNewOrderRouteStock();
  updateNewOrderCapacityBadge();
}

function updateNewOrderCalcPreview(){
  const p = selectedPackaging();
  const palletsRaw = parseFloat(String($('newOrderPalletsQty')?.value || '0').replace(',', '.')) || 0;
  if (!p || palletsRaw <= 0) {
    if ($('newOrderCalcPreview')) $('newOrderCalcPreview').value = 'Paquetes / Botellas';
    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 ($('newOrderCalcPreview')) {
    $('newOrderCalcPreview').value = `${totalPackages.toLocaleString('es-MX')} paquetes · ${totalBottles.toLocaleString('es-MX')} botellas`;
  }
}

async function addNewOrderItem(){
  const r = newOrderSelectedProduct();
  const p = selectedPackaging();
  const palletsRaw = parseFloat(String($('newOrderPalletsQty').value || '0').replace(',', '.')) || 0;
  if (!r) { $('newOrderResult').innerHTML = '<div class="alert alert-danger">Selecciona un artículo.</div>'; return; }
  if (!p) { $('newOrderResult').innerHTML = '<div class="alert alert-danger">Selecciona un embalaje configurado en Admin.</div>'; return; }
  if (palletsRaw <= 0) { $('newOrderResult').innerHTML = '<div class="alert alert-danger">Captura la cantidad de tarimas.</div>'; return; }
  if (!Number.isInteger(palletsRaw)) {
    showBotellaPalletValidation(`La cantidad capturada para ${r.sku} genera una tarima decimal (${palletsRaw.toFixed(3)}). No se permiten tarimas decimales; revisa antes de generar la orden.`);
    return;
  }

  const capacity = vehicleCapacity();
  if (!newOrderIsRoute() && totalNewOrderPallets() + palletsRaw > capacity) {
    $('newOrderResult').innerHTML = `<div class="alert alert-danger">La unidad seleccionada permite ${capacity} tarimas. Captura actual: ${totalNewOrderPallets() + palletsRaw} tarimas.</div>`;
    return;
  }

  const packagesPerPallet = Number(p.packages_per_pallet || 0);
  const piecesPerPackage = Number(p.pieces_per_package || 0);
  const packagesQty = palletsRaw * packagesPerPallet;
  const qtyBottles = packagesQty * piecesPerPackage;
  const ocLabels = ocRelationLabels();
  let relation = '';
  let relationLabel = '';
  let itemOC = '';
  let ocFile = null;
  let ocMissingReason = '';
  let ocFileIndex = null;
  let ocCtx = null;
  if (!newOrderIsRoute()) {
    itemOC = (($('newOrderItemPurchaseOrder')?.value || '').trim() || ($('newOrderPurchaseOrder')?.value || '').trim());
    const ocAlreadyHasDocument = isOcDocumentAlreadyCaptured(itemOC);
    if (!ocAlreadyHasDocument) {
      ocFile = $('newOrderItemOcFile')?.files?.[0] || null;
      ocMissingReason = ($('newOrderItemOcMissingReason')?.value || '').trim();
    }
    if (!itemOC) {
      $('newOrderResult').innerHTML = '<div class="alert alert-danger">Captura la OC del cliente para la OSV.</div>';
      return;
    }
    if (!$('newOrderPurchaseOrder')?.value) $('newOrderPurchaseOrder').value = itemOC;
    ocCtx = getContextForOc(itemOC);
    if (!ocCtx && shouldPromptForNewOc(itemOC)) {
      $('newOrderResult').innerHTML = '<div class="alert alert-info">Confirma la relación de la nueva OC para continuar.</div>';
      ocCtx = await promptOcChangeContext(itemOC);
      if (!ocCtx) return;
    }
    if (!ocCtx) {
      ocCtx = headerOcContext();
      ensureOcContextStore()[normalizeOcValue(itemOC)] = ocCtx;
    }
    if (ocAlreadyHasDocument) {
      ocMissingReason = String(ocCtx.oc_attachment_missing_reason || '');
      ocFileIndex = (ocCtx.oc_file_index === null || ocCtx.oc_file_index === undefined) ? null : ocCtx.oc_file_index;
    }
    applyOcContextToHiddenFields(ocCtx);
    relation = ocCtx.relation || 'MISMO_CLIENTE_MISMA_DIRECCION';
    relationLabel = ocCtx.relation_label || ocLabels[relation] || '';
    if (!Number(ocCtx.customer_id || 0) || !Number(ocCtx.address_id || 0)) {
      $('newOrderResult').innerHTML = '<div class="alert alert-danger">Selecciona cliente y punto de entrega para esta OC.</div>';
      return;
    }
  }

  if (ocFile) {
    ocFileIndex = (window.newOrderOcFiles = window.newOrderOcFiles || []).push(ocFile) - 1;
  }
  if (!newOrderIsRoute() && itemOC) {
    const key = normalizeOcValue(itemOC);
    const store = ensureOcContextStore();
    store[key] = Object.assign({}, ocCtx || headerOcContext(), {
      oc_file_index: (ocFileIndex === null || ocFileIndex === undefined) ? null : ocFileIndex,
      oc_attachment_missing_reason: ocMissingReason || '',
      oc_document_registered: true
    });
    ocCtx = store[key];
  }

  const item = {
    product_id: Number(r.product_id),
    packaging_option_id: Number(p.id),
    sku: String(r.sku || ''),
    name: String(r.name || ''),
    packaging_name: String(p.packaging_name || p.packaging_type || ''),
    packages_per_pallet: packagesPerPallet,
    pieces_per_package: piecesPerPackage,
    packages_qty: packagesQty,
    pallets_qty: palletsRaw,
    qty: qtyBottles,
    pallet_type: $('newOrderPalletType').value.trim(),
    pallet_specification: $('newOrderPalletSpec').value.trim(),
    item_purchase_order: itemOC,
    oc_relation: relation,
    oc_relation_label: relationLabel,
    oc_attachment_missing_reason: ocMissingReason,
    oc_file_index: ocFileIndex,
    item_customer_id: newOrderIsRoute() ? 0 : Number(ocCtx?.customer_id || 0),
    item_address_id: newOrderIsRoute() ? 0 : Number(ocCtx?.address_id || 0),
    customer_label: newOrderIsRoute() ? '' : String(ocCtx?.customer_label || ''),
    item_delivery_address: newOrderIsRoute() ? '' : String(ocCtx?.delivery_address || ''),
    available: num(r.on_hand),
    insufficient_stock: qtyBottles > Math.floor(num(r.on_hand || 0)),
    stock_shortage: Math.max(0, qtyBottles - Math.floor(num(r.on_hand || 0)))
  };
  if (newOrderIsRoute()) {
    const existing = newOrderItems.find(x =>
      Number(x.product_id) === Number(item.product_id) &&
      Number(x.packaging_option_id || 0) === Number(item.packaging_option_id || 0) &&
      String(x.pallet_type || '') === String(item.pallet_type || '') &&
      String(x.pallet_specification || '') === String(item.pallet_specification || '')
    );
    if (existing) {
      existing.pallets_qty = Number(existing.pallets_qty || 0) + Number(item.pallets_qty || 0);
      existing.packages_qty = Number(existing.packages_qty || 0) + Number(item.packages_qty || 0);
      existing.qty = Number(existing.qty || 0) + Number(item.qty || 0);
      existing.available = num(r.on_hand);
      existing.insufficient_stock = Number(existing.qty || 0) > Math.floor(num(r.on_hand || 0));
      existing.stock_shortage = Math.max(0, Number(existing.qty || 0) - Math.floor(num(r.on_hand || 0)));
      item.insufficient_stock = existing.insufficient_stock;
      item.stock_shortage = existing.stock_shortage;
    } else {
      newOrderItems.push(item);
    }
  } else {
    newOrderItems.push(item);
  }
  if (item.insufficient_stock) {
    const warnKey = newOrderStockWarningKey(item.product_id, newOrderType(), 'INSUFFICIENT_ADD_' + Math.round(item.qty || 0));
    if (!newOrderStockWarningShown[warnKey]) {
      newOrderStockWarningShown[warnKey] = true;
      showNewOrderStockWarning(r, item.qty);
    }
  }
  $('newOrderPalletsQty').value = '';
  if ($('newOrderItemOcFile')) $('newOrderItemOcFile').value = '';
  if ($('newOrderItemOcMissingReason')) $('newOrderItemOcMissingReason').value = '';
  updateOcAttachmentFieldsVisibility();
  updateNewOrderCalcPreview();
  $('newOrderResult').innerHTML = '';
  renderNewOrderItems();
  renderNewOrderRouteStock();
  updateNewOrderCapacityBadge();
}

function newOrderHasUnsavedChanges(){
  if (!$('newOrderModal') || !$('newOrderModal').classList.contains('show')) return false;
  if (pendingNewOrderPayload) return true;
  if ((newOrderItems || []).length > 0) return true;
  const fields = [
    'newOrderPurchaseOrder','newOrderCustomerId','newOrderAddressId','newOrderAddress',
    'newOrderTime','newOrderComments','newOrderRoute','newOrderItemPurchaseOrder',
    'newOrderPalletsQty','newOrderItemOcMissingReason'
  ];
  return fields.some(id => {
    const el = $(id);
    if (!el) return false;
    const val = String(el.value || '').trim();
    if (id === 'newOrderVehicleUnit' && val === 'Caja seca') return false;
    return val !== '';
  }) || Boolean($('newOrderItemOcFile')?.files?.length || $('newOrderOcFile')?.files?.length);
}

function requestCloseNewOrder(){
  if (!newOrderHasUnsavedChanges()) {
    allowNewOrderClose = true;
    newOrderModal.hide();
    setTimeout(() => { allowNewOrderClose = false; }, 250);
    return;
  }
  if (newOrderCloseConfirmModal) newOrderCloseConfirmModal.show();
}

function confirmCloseNewOrderWithoutSaving(){
  allowNewOrderClose = true;
  if (newOrderCloseConfirmModal) newOrderCloseConfirmModal.hide();
  if (newOrderModal) newOrderModal.hide();
  pendingNewOrderPayload = null;
  setTimeout(() => { allowNewOrderClose = false; }, 300);
}

async function openNewWarehouseOrder(){
  window.newOrderOcFiles = [];
  window.newOrderOcContexts = {};
  newOrderStockWarningShown = {};
  $('newOrderId').value = '';
  $('newOrderType').value = 'CLIENTE';
  $('newOrderCustomerId').value = '';
  $('newOrderPurchaseOrder').value = '';
  if ($('newOrderOcFile')) $('newOrderOcFile').value = '';
  $('newOrderVehicleUnit').value = 'Caja seca';
  updateVehicleCapacity();
  $('newOrderDate').value = todayIso();
  $('newOrderTime').value = '';
  if ($('newOrderAddressId')) $('newOrderAddressId').innerHTML = '<option value="">Selecciona punto de entrega</option>';
  $('newOrderAddress').value = '';
  $('newOrderComments').value = '';
  $('newOrderRoute').value = '';
  syncRouteLabel();
  updateNewOrderMode();
  $('btnNewOrderSave').innerHTML = $('newOrderId').value ? '<i class="bi bi-check-circle me-1"></i>Actualizar orden' : '<i class="bi bi-check-circle me-1"></i>Crear orden';
  $('newOrderResult').innerHTML = '';
  newOrderItems = [];
  newOrderRouteStockRows = [];
  renderNewOrderItems();
  renderNewOrderRouteStock();

  if (!inventoryLocations.length) {
    await loadLocations();
  }

  $('newOrderLocation').innerHTML = inventoryLocations.map(l => `
    <option value="${Number(l.id)}">${h(l.name)} ${l.code ? '(' + h(l.code) + ')' : ''}</option>
  `).join('');

  const filterLoc = $('fLocation')?.value || '';
  if (filterLoc) {
    $('newOrderLocation').value = filterLoc;
  }

  await Promise.all([loadNewOrderStock(), loadNewOrderCustomers(), loadNewOrderRoutes()]);
  newOrderModal.show();
}


async function openEditWarehouseOrder(orderId){
  const j = await apiGet('order_detail', {order_id: orderId});
  if(!j.ok){ alert(j.error || 'No se pudo cargar el pedido'); return; }
  const o = j.order || {}; const items = j.items || [];
  await openNewWarehouseOrder();
  $('newOrderId').value = String(o.id || '');
  $('newOrderType').value = String(o.source || '').toUpperCase().includes('RUTA') || String(o.sales_order_ref || '').toUpperCase().startsWith('OSA-') ? 'RUTA' : 'CLIENTE';
  updateNewOrderMode();
  $('newOrderLocation').value = String(o.location_id || '');
  $('newOrderCustomerId').value = o.customer_id ? String(o.customer_id) : '';
  $('newOrderPurchaseOrder').value = o.purchase_order || '';
  $('newOrderVehicleUnit').value = o.vehicle_unit || '';
  $('newOrderDate').value = o.requested_delivery_date || todayIso();
  $('newOrderTime').value = o.requested_delivery_time || '';
  $('newOrderRoute').value = o.route_no ? String(o.route_no) : '';
  await loadNewOrderRouteStock();
  await loadNewOrderCustomerAddresses(o.address_id ? String(o.address_id) : '');
  $('newOrderAddress').value = o.delivery_address || $('newOrderAddress').value || '';
  $('newOrderComments').value = (o.comments || '').replace('Orden de salida creada desde módulo Ventas.','').replace(/Ruta\s+\d+/,'').trim();
  await loadNewOrderStock();
  newOrderItems = items.map(it => {
    const stock = (newOrderStockRows || []).find(r => Number(r.product_id) === Number(it.product_id));
    return {
      product_id: Number(it.product_id),
      packaging_option_id: Number(it.packaging_option_id || 0),
      sku: String(it.sku || ''),
      name: String(it.product_name || ''),
      packaging_name: String(it.packaging_name || it.packaging_type || ''),
      packages_per_pallet: Number(it.packages_per_pallet || 0),
      pieces_per_package: Number(it.pieces_per_package || 0),
      packages_qty: Number(it.packages_qty || 0),
      pallets_qty: Number(it.pallets_qty || 0),
      qty: num(it.qty),
      pallet_type: String(it.pallet_type || ''),
      pallet_specification: String(it.pallet_specification || ''),
      item_purchase_order: String(it.purchase_order || o.purchase_order || ''),
      oc_relation: String(it.oc_relation || ''),
      oc_relation_label: String(it.oc_relation || ''),
      oc_attachment_missing_reason: String(it.oc_attachment_missing_reason || ''),
      oc_file_index: null,
      item_customer_id: Number(it.item_customer_id || o.customer_id || 0),
      item_address_id: Number(it.item_address_id || o.address_id || 0),
      customer_label: String(it.customer_label || o.business_name || ''),
      item_delivery_address: String(it.item_delivery_address || o.delivery_address || ''),
      available: stock ? num(stock.on_hand) : 0,
      insufficient_stock: num(it.qty) > Math.floor(stock ? num(stock.on_hand) : 0),
      stock_shortage: Math.max(0, num(it.qty) - Math.floor(stock ? num(stock.on_hand) : 0))
    };
  });
  renderNewOrderItems();
  $('btnNewOrderSave').innerHTML = '<i class="bi bi-check-circle me-1"></i>Actualizar orden';
  $('newOrderResult').innerHTML = '<div class="alert alert-info mb-0">Editando orden. Al guardar se reemplazan los artículos actuales.</div>';
}

function buildNewOrderPayload(){
  const locationId = parseInt($('newOrderLocation').value || '0', 10);
  return {
    order_id: parseInt($('newOrderId').value || '0', 10),
    order_type: newOrderType(),
    location_id: locationId,
    route_no: parseInt($('newOrderRoute').value || '0', 10),
    customer_id: newOrderIsRoute() ? 0 : parseInt($('newOrderCustomerId').value || '0', 10),
    address_id: newOrderIsRoute() ? 0 : parseInt($('newOrderAddressId')?.value || '0', 10),
    requested_delivery_date: $('newOrderDate').value,
    requested_delivery_time: $('newOrderTime').value,
    delivery_address: newOrderIsRoute() ? '' : $('newOrderAddress').value.trim(),
    purchase_order: newOrderIsRoute() ? '' : $('newOrderPurchaseOrder').value.trim(),
    vehicle_unit: newOrderIsRoute() ? 'Unidad de reparto a granel' : $('newOrderVehicleUnit').value.trim(),
    comments: $('newOrderComments').value.trim(),
    items: newOrderItems.map(it => ({
      product_id: it.product_id,
      packaging_option_id: it.packaging_option_id,
      pallets_qty: it.pallets_qty,
      pallet_type: it.pallet_type,
      pallet_specification: it.pallet_specification,
      item_purchase_order: it.item_purchase_order || '',
      oc_relation: it.oc_relation || '',
      oc_attachment_missing_reason: it.oc_attachment_missing_reason || '',
      oc_file_index: (it.oc_file_index === null || it.oc_file_index === undefined) ? null : it.oc_file_index,
      item_customer_id: it.item_customer_id || 0,
      item_address_id: it.item_address_id || 0,
      customer_label: it.customer_label || '',
      item_delivery_address: it.item_delivery_address || '' 
    }))
  };
}

function newOrderLocationLabel(locationId){
  const loc = inventoryLocations.find(l => Number(l.id) === Number(locationId));
  if (!loc) return '—';
  return `${loc.name || ''}${loc.code ? ' (' + loc.code + ')' : ''}`;
}

function renderNewOrderConfirm(payload){
  const isUpdate = Number(payload.order_id) > 0;
  const routeLabel = $('newOrderRoute').selectedOptions[0]?.textContent || 'Sin ruta';
  const itemsHtml = newOrderItems.length ? newOrderItems.map(it => `
    <tr>
      <td class="mono">${h(it.sku)}</td>
      <td>${h(it.name)}</td>
      <td>${h(it.packaging_name)}</td>
      <td class="text-end mono fw-bold">${h(it.pallets_qty)}</td>
      <td class="text-end mono">${h(it.packages_per_pallet)}</td>
      <td class="text-end mono fw-bold">${h(it.packages_qty)}</td>
      <td class="text-end mono fw-bold">${h(it.qty)}</td>
    </tr>
  `).join('') : '<tr><td colspan="7" class="text-center muted py-3">Agrega artículos por presentación antes de crear la orden.</td></tr>';

  $('newOrderConfirmTitle').textContent = isUpdate ? 'Confirmar actualización de orden' : 'Confirmar generación de pedido';
  const stockWarnings = (newOrderItems || []).filter(it => it.insufficient_stock);
  const stockWarningsHtml = stockWarnings.length ? `
    <div class="alert alert-warning mb-3">
      <div class="fw-bold"><i class="bi bi-exclamation-triangle me-1"></i>Hay artículos sin existencia suficiente</div>
      <div class="small mt-1">Puedes generar la orden, pero se debe avisar que se necesita existencia antes de surtir.</div>
      <ul class="mb-0 mt-2 small">${stockWarnings.map(it => `<li><strong>${h(it.sku)}</strong>: solicitado ${h(qty(it.qty || 0))}, disponible ${h(qty(it.available || 0))}, faltante ${h(qty(it.stock_shortage || 0))} botellas.</li>`).join('')}</ul>
    </div>` : '';
  $('newOrderConfirmBody').innerHTML = `
    <div class="fw-bold mb-2">${isUpdate ? '¿Deseas actualizar esta orden de salida?' : '¿Deseas generar esta orden de salida?'}</div>
    <div class="row g-2 mb-3">
      <div class="col-12 col-md-6"><strong>Planta:</strong> ${h(newOrderLocationLabel(payload.location_id))}</div>
      <div class="col-12 col-md-6"><strong>Ruta:</strong> ${h(routeLabel)}</div>
      <div class="col-12 col-md-6"><strong>Cliente:</strong> ${h($('newOrderCustomerId').selectedOptions[0]?.textContent || '—')}</div>
      <div class="col-12 col-md-6"><strong>Fecha:</strong> ${h(payload.requested_delivery_date || '—')} ${h(payload.requested_delivery_time || '')}</div>
      <div class="col-12"><strong>Destino:</strong> ${h(payload.delivery_address || '—')}<br><strong>OC:</strong> ${h(payload.purchase_order || '—')} · <strong>Unidad:</strong> ${h(payload.vehicle_unit || '—')}</div>
    </div>
    <div class="table-wrap">
      <div class="table-responsive">
        <table class="table table-hover align-middle mb-0">
          <thead><tr><th>SKU</th><th>Artículo</th><th>Embalaje</th><th class="text-end">Tarimas</th><th class="text-end">Paq/Tar</th><th class="text-end">Total paquetes</th><th class="text-end">Botellas</th></tr></thead>
          <tbody>${itemsHtml}</tbody>
        </table>
      </div>
    </div>
    ${stockWarningsHtml}
    <div class="mt-3 small muted">Al confirmar, la orden quedará guardada y será visible para Almacén. Todavía no se descuenta inventario hasta que se genere la salida.</div>
  `;
}

async function saveNewWarehouseOrder(){
  const payload = buildNewOrderPayload();
  if (payload.location_id <= 0) {
    $('newOrderResult').innerHTML = `<div class="alert alert-danger">Selecciona la planta / ubicación.</div>`;
    return;
  }
  if (payload.order_type === 'CLIENTE' && !payload.purchase_order) {
    $('newOrderResult').innerHTML = `<div class="alert alert-danger">Captura la OC del cliente para la OSV.</div>`;
    return;
  }
  if (payload.order_type === 'CLIENTE' && !payload.customer_id) {
    $('newOrderResult').innerHTML = `<div class="alert alert-danger">Selecciona un cliente registrado.</div>`;
    return;
  }
  if (payload.order_type === 'CLIENTE' && (!payload.address_id || !payload.delivery_address)) {
    $('newOrderResult').innerHTML = `<div class="alert alert-danger">Selecciona el punto de entrega del cliente.</div>`;
    return;
  }
  if (payload.order_type === 'RUTA' && !payload.route_no) {
    $('newOrderResult').innerHTML = `<div class="alert alert-danger">Selecciona una ruta de reparto.</div>`;
    return;
  }
  if (!newOrderItems.length) {
    $('newOrderResult').innerHTML = `<div class="alert alert-danger">Agrega al menos un artículo por tarimas.</div>`;
    return;
  }
  if (!newOrderIsRoute() && totalNewOrderPallets() > vehicleCapacity()) {
    $('newOrderResult').innerHTML = `<div class="alert alert-danger">La carga excede la capacidad de la unidad seleccionada.</div>`;
    return;
  }
  pendingNewOrderPayload = payload;
  $('newOrderResult').innerHTML = '';
  renderNewOrderConfirm(payload);
  newOrderConfirmModal.show();
}

async function executeNewWarehouseOrderSave(){
  const payload = pendingNewOrderPayload;
  if (!payload) {
    newOrderConfirmModal.hide();
    return;
  }

  const isUpdate = Number(payload.order_id) > 0;
  $('btnNewOrderConfirmYes').disabled = true;
  $('btnNewOrderConfirmYes').innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Procesando...';
  $('btnNewOrderSave').disabled = true;
  $('btnNewOrderSave').innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Procesando...';

  let j;
  const headerOcFile = $('newOrderOcFile')?.files?.[0] || null;
  const hasItemFiles = payload.items.some(it => it.oc_file_index !== null && it.oc_file_index !== undefined);
  if (payload.order_type === 'CLIENTE' && (headerOcFile || hasItemFiles)) {
    const fd = new FormData();
    fd.append('__payload', JSON.stringify(payload));
    if (headerOcFile) fd.append('new_order_oc_file', headerOcFile, headerOcFile.name || 'oc_cliente');
    (window.newOrderOcFiles || []).forEach((file, idx) => {
      if (file) fd.append('new_order_item_oc_file_' + idx, file, file.name || ('oc_cliente_' + idx));
    });
    j = await apiPostForm('manual_order_create', fd);
  } else {
    j = await apiPost('manual_order_create', payload);
  }

  $('btnNewOrderConfirmYes').disabled = false;
  $('btnNewOrderConfirmYes').innerHTML = '<i class="bi bi-check-circle me-1"></i>Sí, procesar';
  $('btnNewOrderSave').disabled = false;
  $('btnNewOrderSave').innerHTML = isUpdate ? '<i class="bi bi-check-circle me-1"></i>Actualizar orden' : '<i class="bi bi-check-circle me-1"></i>Crear orden';

  if (!j.ok) {
    newOrderConfirmModal.hide();
    if (j.validation_code === 'PALLET_DECIMAL') {
      showBotellaPalletValidation(j.error || 'La cantidad genera una tarima decimal; revisa la captura.');
    } else {
      $('newOrderResult').innerHTML = `<div class="alert alert-danger">${h(j.error || 'No se pudo crear la orden')}</div>`;
    }
    return;
  }

  newOrderConfirmModal.hide();
  const routeLabel = j.route_label || ($('newOrderRoute').selectedOptions[0]?.textContent || '—');
  const itemsHtml = newOrderItems.length ? newOrderItems.map(it => `
    <tr>
      <td class="mono">${h(it.sku)}</td>
      <td>${h(it.name)}</td>
      <td>${h(it.packaging_name)}</td>
      <td class="text-end mono fw-bold">${h(it.pallets_qty)}</td>
      <td class="text-end mono">${h(it.packages_per_pallet)}</td>
      <td class="text-end mono fw-bold">${h(it.packages_qty)}</td>
      <td class="text-end mono fw-bold">${h(it.qty)}</td>
    </tr>
  `).join('') : '<tr><td colspan="7" class="text-center muted py-3">Agrega artículos por presentación antes de crear la orden.</td></tr>';

  $('newOrderSummaryBody').innerHTML = `
    <div class="alert alert-success mb-3">
      <div class="fw-bold">${h(j.message || (isUpdate ? 'Orden actualizada correctamente.' : 'Orden creada correctamente.'))}</div>
      <div>Folio: <span class="mono">${h(j.folio || '')}</span></div>
      <div>Ruta: <strong>${h(routeLabel)}</strong></div>
      <div>Cliente / destino: <strong>${h(payload.customer_name || 'Orden de salida Ventas')}</strong></div>
      <div>Fecha solicitada: <strong>${h(payload.requested_delivery_date || '—')} ${h(payload.requested_delivery_time || '')}</strong></div>
      ${(j.warnings && j.warnings.length) ? `<div class="mt-3 alert alert-warning mb-0"><b>Orden creada, pero hay aviso de adjunto:</b><br>${j.warnings.map(w => h(w)).join('<br>')}</div>` : ''}
      <div class="mt-3 alert alert-info mb-0">La orden quedó autorizada y disponible para Almacén. La carga de unidad y la salida física se realizarán desde Almacén.</div>
    </div>
    <div class="section-title mb-2"><i class="bi bi-list-check text-primary"></i> Resumen de artículos solicitados</div>
    <div class="table-wrap">
      <div class="table-responsive">
        <table class="table table-hover align-middle mb-0">
          <thead><tr><th>SKU</th><th>Artículo</th><th>Embalaje</th><th class="text-end">Tarimas</th><th class="text-end">Paq/Tar</th><th class="text-end">Total paquetes</th><th class="text-end">Botellas</th></tr></thead>
          <tbody>${itemsHtml}</tbody>
        </table>
      </div>
    </div>
    <div class="mt-3 small muted">Almacén podrá ver esta orden en su sección de pedidos autorizados. El descuento de inventario se realizará cuando se genere la salida.</div>
  `;

  await loadOrders();
  newOrderSummaryModal.show();
}

async function finishNewOrderSummary(){
  newOrderSummaryModal.hide();
  allowNewOrderClose = true;
  newOrderModal.hide();
  setTimeout(() => { allowNewOrderClose = false; }, 300);
  pendingNewOrderPayload = null;
  newOrderItems = [];
  newOrderRouteStockRows = [];
  renderNewOrderItems();
  renderNewOrderRouteStock();
  await loadOrders();
}

function openDispatchConfirm(orderId){
  const row = currentRows.find(x => Number(x.id) === Number(orderId));
  if(!row){
    alert('No se encontró el pedido en pantalla. Recarga e intenta de nuevo.');
    return;
  }
  const folio = row.sales_order_ref ? row.sales_order_ref : ('PED-' + row.id);
  $('dispatchOrderId').value = String(orderId);
  $('dispatchNotes').value = '';
  $('dispatchWarnings').innerHTML = '';
  $('btnDispatchPdf').classList.add('d-none');
  $('btnDispatchPdf').setAttribute('href', '#');
  $('btnConfirmDispatch').classList.remove('d-none');
  $('btnConfirmDispatch').disabled = false;
  $('dispatchTitle').textContent = 'Confirmar salida de almacén';
  $('dispatchSubtitle').textContent = 'Confirma para descontar inventario y dejar el PDF listo.';
  $('dispatchBody').innerHTML = `
    <div class="fw-bold mb-2">Pedido ${h(folio)}</div>
    <div>Cliente: <strong>${h(row.business_name || '—')}</strong></div>
    <div>Fecha solicitada: <strong>${h(row.requested_delivery_date || '—')} ${h(row.requested_delivery_time || '')}</strong></div>
    <div class="mt-2">Al confirmar, Ventas generará la salida, descontará inventario disponible y mostrará el botón para descargar el PDF.</div>
  `;
  dispatchModal.show();
}

async function confirmSalesDispatch(){
  const orderId = parseInt($('dispatchOrderId').value || '0', 10);
  if(orderId <= 0){
    alert('Pedido inválido');
    return;
  }

  $('btnConfirmDispatch').disabled = true;
  $('btnConfirmDispatch').innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Generando...';

  const j = await apiPost('sales_dispatch_from_sales', {
    order_id: orderId,
    notes: $('dispatchNotes').value.trim()
  });

  $('btnConfirmDispatch').innerHTML = '<i class="bi bi-check-circle me-1"></i>Confirmar y generar salida';

  if(!j.ok){
    $('btnConfirmDispatch').disabled = false;
    $('dispatchWarnings').innerHTML = `<div class="alert alert-danger mb-0">${h(j.error || 'No se pudo generar la salida')}</div>`;
    return;
  }

  const warnings = j.warnings || [];
  $('dispatchTitle').textContent = 'Salida generada';
  $('dispatchSubtitle').textContent = 'El pedido quedó procesado y el PDF está disponible.';
  $('dispatchBody').innerHTML = `
    <div class="fw-bold mb-2">${h(j.message || 'Salida generada correctamente.')}</div>
    <div>Salida: <strong>#${h(j.outbound_id || '')}</strong></div>
    <div class="mt-2">Puedes descargar el PDF de orden de entrega con el botón inferior.</div>
  `;
  $('dispatchWarnings').innerHTML = warnings.length
    ? `<div class="alert alert-warning mb-0"><div class="fw-bold mb-1">Observaciones</div><ul class="mb-0">${warnings.map(w => `<li>${h(w)}</li>`).join('')}</ul></div>`
    : '';
  $('btnConfirmDispatch').classList.add('d-none');
  $('btnDispatchPdf').classList.remove('d-none');
  $('btnDispatchPdf').setAttribute('href', j.pdf_url || '#');

  await loadOrders();
}

function renderInventoryCards(rows){
  const wrap = $('invCardsRow');
  if(!wrap) return;

  const visibles = (rows || [])
    .filter(r => Number(r.active) === 1 && num(r.on_hand) > 0)
    .sort((a,b) => String(a.sku || '').localeCompare(String(b.sku || '')));

  if(!visibles.length){
    wrap.innerHTML = '';
    return;
  }

  wrap.innerHTML = visibles.map(r => {
    const available = isBarSku(r.sku) ? qtyBar(r.on_hand) : qty(r.on_hand);
    const wh = Array.isArray(r.warehouse_breakdown) ? r.warehouse_breakdown : [];
    const chips = wh.length
      ? `<div class="mt-2 d-flex gap-1 flex-wrap">${wh.map(w => `
          <span class="badge rounded-pill text-bg-light border">
            ${h(w.warehouse_code || '')}: <span class="mono">${h(qty(w.qty || 0))}</span>
          </span>
        `).join('')}</div>`
      : '';

    return `
      <div class="col-12 col-md-6 col-xl-4">
        <div class="stock-card h-100">
          <div class="stock-icon"><i class="bi bi-box-seam"></i></div>
          <div class="min-w-0 flex-grow-1">
            <div class="stock-title text-truncate" title="${h(r.sku || '')} · ${h(r.name || '')}">
              ${h(r.sku || '')}
            </div>
            <div class="stock-name text-truncate" title="${h(r.name || '')}">${h(r.name || '')}</div>
            <div class="stock-value mono mt-2">${h(available)}</div>
            <div class="stock-sub">Disponible ${h(r.uom || '')}</div>
            ${chips}
          </div>
        </div>
      </div>
    `;
  }).join('');
}

async function loadInventoryByLocation(){
  const locationId = $('invLocationId').value;
  if(!locationId){
    alert('Selecciona una ubicación');
    return;
  }

  const j = await apiGet('inventory_by_location', {location_id: locationId});
  if(!j.ok){
    alert(j.error || 'No se pudieron cargar las existencias');
    return;
  }

  const rows = j.rows || [];
  const loc = j.location || {};

  $('invLocationInfo').textContent =
    `Ubicación: ${loc.name || ''}${loc.code ? ' (' + loc.code + ')' : ''}. ${j.inventory_note || 'Inventario de venta por almacenes activos de la planta.'}`;

  renderInventoryCards(rows);

  const tb = $('invStockBody');
  tb.innerHTML = '';

  const visibles = rows.filter(r => num(r.on_hand) > 0);

  if(!visibles.length){
    $('invEmptyHint').textContent = 'Sin existencias disponibles en la ubicación seleccionada.';
    return;
  }

  $('invEmptyHint').textContent = '';

  visibles.forEach(r => {
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="mono">${h(r.sku)}</td>
        <td>
          <div class="fw-semibold">${h(r.name)}</div>
          <div class="small muted">${Array.isArray(r.warehouse_breakdown) && r.warehouse_breakdown.length ? r.warehouse_breakdown.map(w => `${h(w.warehouse_code || '')}: ${h(qty(w.qty || 0))}`).join(' · ') : 'Sin almacén disponible'}</div>
        </td>
        <td class="text-end mono fw-bold">${isBarSku(r.sku) ? qtyBar(r.on_hand) : qty(r.on_hand)}</td>
        <td class="mono">${h(r.uom || '')}</td>
        <td>${Number(r.active) === 1 ? '<span class="pill-active">Activo</span>' : '<span class="pill-inactive">Inactivo</span>'}</td>
      </tr>
    `);
  });
}

async function loadCustomers(){
  const locationId = parseInt($('fLocation')?.value || '0', 10);
  const j = await apiGet('customers', { q: $('customerSearch').value.trim(), location_id: locationId });
  if(!j.ok){
    alert(j.error || 'No se pudieron cargar los clientes');
    return;
  }

  const rows = j.rows || [];
  const tb = $('tbCustomers');
  tb.innerHTML = '';

  if(!rows.length){
    $('customersEmptyHint').textContent = 'Sin clientes para el filtro seleccionado.';
    return;
  }

  $('customersEmptyHint').textContent = '';

  rows.forEach(r => {
    const active = Number(r.active) === 1;
    const customerName = encodeURIComponent(String(r.business_name || ''));
    const customerRowPayload = encodeURIComponent(JSON.stringify(r));
    const locLabel = [r.location_name || '', r.location_code ? '(' + r.location_code + ')' : ''].filter(Boolean).join(' ');
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="mono fw-semibold">${h(r.customer_code || '')}</td>
        <td><div class="fw-semibold">${h(r.business_name || '')}</div><div class="small muted">${Number(r.addresses_count || 0)} punto(s) de entrega</div></td>
        <td>${h(r.contact_name || '')}</td>
        <td>${h(r.address || '')}</td>
        <td>${h(locLabel || '—')}</td>
        <td>${active ? '<span class="pill-active">Activo</span>' : '<span class="pill-inactive">Inactivo</span>'}</td>
        <td class="text-end">
          <div class="d-flex gap-2 justify-content-end flex-wrap">
            <button class="btn toolbar-btn btn-sm" type="button" onclick="openCustomerUsers(${Number(r.id)}, decodeURIComponent('${customerName}'))"><i class="bi bi-person-lock me-1"></i>Accesos</button>
            <button class="btn toolbar-btn btn-sm" type="button" onclick="openCustomerAddresses(${Number(r.id)}, decodeURIComponent('${customerName}'), ${Number(r.location_id || 0)})"><i class="bi bi-geo-alt me-1"></i>Puntos</button>
          </div>
        </td>
        <td class="text-end">
          <div class="d-flex gap-2 justify-content-end flex-wrap">
            <button class="btn toolbar-btn btn-sm" type="button" onclick="openCustomerEdit('${customerRowPayload}')"><i class="bi bi-pencil me-1"></i>Editar</button>
            <button class="btn ${active ? 'btn-danger-soft' : 'btn-green-soft'} btn-sm" type="button" onclick="toggleCustomer(${Number(r.id)}, ${active ? 0 : 1})">
              ${active ? '<i class="bi bi-slash-circle me-1"></i>Desactivar' : '<i class="bi bi-check-circle me-1"></i>Activar'}
            </button>
          </div>
        </td>
      </tr>
    `);
  });
}

function fillLocationSelect(el, selected=''){
  el.innerHTML = inventoryLocations.map(l => `<option value="${Number(l.id)}">${h(l.name)} ${l.code ? '(' + h(l.code) + ')' : ''}</option>`).join('');
  if(selected) el.value = String(selected);
}

async function openCustomerEdit(encodedRow=''){
  if(!inventoryLocations.length) await loadLocations();
  const row = encodedRow ? JSON.parse(decodeURIComponent(encodedRow)) : {};
  $('ceCustomerId').value = row.id || '';
  fillLocationSelect($('ceLocationId'), row.location_id || $('fLocation')?.value || '');
  $('ceCustomerCode').value = row.customer_code || '';
  $('ceBusinessName').value = row.business_name || '';
  $('ceContactName').value = row.contact_name || '';
  $('cePhone').value = row.phone || '';
  $('ceEmail').value = row.email || '';
  $('cePostalCode').value = row.postal_code || '';
  $('ceAddress').value = row.address || '';
  $('ceActive').value = Number(row.active) === 0 ? '0' : '1';
  $('customerEditTitle').textContent = row.id ? 'Editar cliente' : 'Nuevo cliente';
  $('customerEditResult').innerHTML = '';
  customerEditModal.show();
}

async function saveCustomer(){
  const payload = {
    customer_id: parseInt($('ceCustomerId').value || '0',10),
    location_id: parseInt($('ceLocationId').value || '0',10),
    customer_code: $('ceCustomerCode').value.trim(),
    business_name: $('ceBusinessName').value.trim(),
    contact_name: $('ceContactName').value.trim(),
    phone: $('cePhone').value.trim(),
    email: $('ceEmail').value.trim(),
    postal_code: $('cePostalCode').value.trim(),
    address: $('ceAddress').value.trim(),
    active: parseInt($('ceActive').value || '1',10)
  };
  const j = await apiPost('customer_save', payload);
  if(!j.ok){
    $('customerEditResult').innerHTML = `<div class="alert alert-danger">${h(j.error || 'No se pudo guardar')}</div>`;
    return;
  }
  $('customerEditResult').innerHTML = '<div class="alert alert-success">Cliente guardado correctamente. También se actualizó su punto de entrega principal.</div>';
  await loadCustomers();
  await loadNewOrderCustomers().catch(() => {});
}

function clearCustomerAddressForm(){
  $('caAddressId').value='';
  $('caLabel').value='';
  $('caAddress').value='';
  $('caPostalCode').value='';
  $('caDefault').value='0';
  $('caActive').value='1';
  if ($('caLocationId')) fillLocationSelect($('caLocationId'), $('fLocation')?.value || '');
}

async function openCustomerAddresses(customerId, customerName, locationId=0){
  if(!inventoryLocations.length) await loadLocations();
  $('caCustomerId').value = String(customerId || '');
  $('caTitleSub').textContent = 'Cliente: ' + String(customerName || '') + '. Registra diferentes puntos de entrega.';
  clearCustomerAddressForm();
  if (locationId > 0 && $('caLocationId')) $('caLocationId').value = String(locationId);
  customerAddressesModal.show();
  await loadCustomerAddresses();
}

async function loadCustomerAddresses(){
  const customerId = parseInt($('caCustomerId').value || '0', 10);
  const j = await apiGet('customer_addresses', {customer_id: customerId});
  if(!j.ok){ alert(j.error || 'No se pudieron cargar puntos de entrega'); return; }

  const rows = j.rows || [];
  const tb = $('tbCustomerAddresses');
  tb.innerHTML = '';

  if(!rows.length){
    $('customerAddressesEmptyHint').textContent = 'Sin puntos de entrega registrados.';
    return;
  }

  $('customerAddressesEmptyHint').textContent = '';

  rows.forEach(r => {
    const active = Number(r.active) === 1;
    const safeRow = encodeURIComponent(JSON.stringify(r));
    const locLabel = [r.location_name || '', r.location_code ? '(' + r.location_code + ')' : ''].filter(Boolean).join(' ');
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td>${h(r.label || '')}</td>
        <td>${h(locLabel || '—')}</td>
        <td>${h(r.address || '')}</td>
        <td class="mono">${h(r.postal_code || '')}</td>
        <td>${Number(r.is_default) === 1 ? '<span class="pill-active">Sí</span>' : 'No'}</td>
        <td>${active ? '<span class="pill-active">Activo</span>' : '<span class="pill-inactive">Inactivo</span>'}</td>
        <td class="text-end">
          <div class="d-flex gap-2 justify-content-end flex-wrap">
            <button class="btn toolbar-btn btn-sm" type="button" onclick="fillCustomerAddressForm('${safeRow}')">Editar</button>
            <button class="btn ${active ? 'btn-danger-soft' : 'btn-green-soft'} btn-sm" type="button" onclick="toggleCustomerAddress(${Number(r.id)}, ${active ? 0 : 1})">${active ? 'Desactivar' : 'Activar'}</button>
          </div>
        </td>
      </tr>
    `);
  });
}

function fillCustomerAddressForm(safeRow){
  const r = JSON.parse(decodeURIComponent(safeRow));
  $('caAddressId').value = r.id || '';
  $('caLabel').value = r.label || '';
  $('caAddress').value = r.address || '';
  $('caPostalCode').value = r.postal_code || '';
  $('caDefault').value = Number(r.is_default) === 1 ? '1' : '0';
  $('caActive').value = Number(r.active) === 1 ? '1' : '0';
  if ($('caLocationId')) fillLocationSelect($('caLocationId'), r.location_id || $('fLocation')?.value || '');
}

async function saveCustomerAddress(){
  const j = await apiPost('customer_address_save', {
    address_id: parseInt($('caAddressId').value || '0',10),
    customer_id: parseInt($('caCustomerId').value || '0',10),
    location_id: parseInt($('caLocationId')?.value || '0',10),
    label: $('caLabel').value.trim(),
    address: $('caAddress').value.trim(),
    postal_code: $('caPostalCode').value.trim(),
    is_default: parseInt($('caDefault').value || '0',10),
    active: parseInt($('caActive').value || '1',10)
  });
  if(!j.ok){ alert(j.error || 'No se pudo guardar punto de entrega'); return; }
  clearCustomerAddressForm();
  await loadCustomerAddresses();
}

async function toggleCustomerAddress(addressId, active){
  const j = await apiPost('customer_address_toggle', {address_id: addressId, active: active});
  if(!j.ok){ alert(j.error || 'No se pudo actualizar punto de entrega'); return; }
  await loadCustomerAddresses();
}

async function toggleCustomer(customerId, active){
  const ok = confirm(active === 1 ? '¿Activar este cliente?' : '¿Desactivar este cliente?');
  if(!ok) return;

  const j = await apiPost('customer_toggle', {
    customer_id: customerId,
    active: active
  });

  if(!j.ok){
    alert(j.error || 'No se pudo actualizar el cliente');
    return;
  }

  await loadCustomers();
}

function clearCustomerUserForm(){
  $('cuEditingUserId').value = '';
  $('cuFullName').value = '';
  $('cuPhone').value = '';
  $('cuEmail').value = '';
  $('cuRole').value = 'cliente';
  $('cuPassword').value = '';
  $('cuActive').value = '1';
}

function fillCustomerUserForm(row){
  $('cuEditingUserId').value = String(row.id || '');
  $('cuFullName').value = row.full_name || '';
  $('cuPhone').value = row.phone || '';
  $('cuEmail').value = row.email || '';
  $('cuRole').value = row.role || 'cliente';
  $('cuPassword').value = '';
  $('cuActive').value = String(Number(row.active) === 1 ? 1 : 0);
}

async function openCustomerUsers(customerId, customerName){
  currentCustomerAccess = { customerId: Number(customerId || 0), customerName: String(customerName || '') };
  $('cuCustomerId').value = String(currentCustomerAccess.customerId || '');
  $('customerUsersTitleSub').textContent = 'Cliente: ' + currentCustomerAccess.customerName + '. Alta, edición, contraseña y activación del acceso del cliente.';
  clearCustomerUserForm();
  customerUsersModal.show();
  await loadCustomerUsers();
}

async function loadCustomerUsers(){
  const customerId = parseInt($('cuCustomerId').value || '0', 10);
  if(customerId <= 0){
    $('tbCustomerUsers').innerHTML = '';
    $('customerUsersEmptyHint').textContent = 'Selecciona un cliente válido.';
    return;
  }

  const j = await apiGet('customer_users', { customer_id: customerId });
  if(!j.ok){
    alert(j.error || 'No se pudieron cargar los accesos del cliente');
    return;
  }

  currentCustomerUsers = j.rows || [];
  const tb = $('tbCustomerUsers');
  tb.innerHTML = '';

  if(!currentCustomerUsers.length){
    $('customerUsersEmptyHint').textContent = 'Este cliente aún no tiene usuarios de acceso.';
    return;
  }

  $('customerUsersEmptyHint').textContent = '';

  currentCustomerUsers.forEach(r => {
    const active = Number(r.active) === 1;
    const safeRow = encodeURIComponent(JSON.stringify(r));
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td><div class="fw-semibold">${h(r.full_name || '')}</div></td>
        <td>
          <div class="mono">${h(r.phone || '')}</div>
          <div class="small">Usuario: ${h(r.username || r.phone || '')}</div>
        </td>
        <td>${h(customerRoleLabel(r.role))}</td>
        <td>${h(r.email || '')}</td>
        <td>${active ? '<span class="pill-active">Activo</span>' : '<span class="pill-inactive">Inactivo</span>'}</td>
        <td class="mono">${r.last_login_at ? h(dt(r.last_login_at)) : '—'}</td>
        <td class="text-end">
          <div class="d-flex gap-2 justify-content-end flex-wrap">
            <button class="btn toolbar-btn btn-sm" type="button" onclick="editCustomerUser('${safeRow}')">
              <i class="bi bi-pencil-square me-1"></i>Editar
            </button>
            <button class="btn btn-warning-soft btn-sm" type="button" onclick="changeCustomerUserPassword(${Number(r.id)})">
              <i class="bi bi-key me-1"></i>Contraseña
            </button>
            <button class="btn ${active ? 'btn-danger-soft' : 'btn-green-soft'} btn-sm" type="button" onclick="toggleCustomerUser(${Number(r.id)}, ${active ? 0 : 1})">
              ${active ? '<i class="bi bi-slash-circle me-1"></i>Deshabilitar' : '<i class="bi bi-check-circle me-1"></i>Habilitar'}
            </button>
          </div>
        </td>
      </tr>
    `);
  });
}

function editCustomerUser(encoded){
  try{
    const row = JSON.parse(decodeURIComponent(encoded));
    fillCustomerUserForm(row);
  }catch(e){
    console.error(e);
    alert('No se pudo cargar el usuario para edición');
  }
}

async function saveCustomerUser(){
  const customerId = parseInt($('cuCustomerId').value || '0', 10);
  const userId = parseInt($('cuEditingUserId').value || '0', 10);
  const payload = {
    customer_id: customerId,
    full_name: $('cuFullName').value.trim(),
    phone: $('cuPhone').value.trim(),
    email: $('cuEmail').value.trim(),
    role: $('cuRole').value,
    active: parseInt($('cuActive').value || '1', 10)
  };

  if(userId > 0){
    payload.user_id = userId;
    const j = await apiPost('customer_user_update', payload);
    if(!j.ok){
      alert(j.error || 'No se pudo actualizar el usuario');
      return;
    }
    alert('Usuario actualizado correctamente');
  } else {
    payload.password = $('cuPassword').value;
    const j = await apiPost('customer_user_create', payload);
    if(!j.ok){
      alert(j.error || 'No se pudo crear el usuario');
      return;
    }
    alert('Usuario creado correctamente');
  }

  clearCustomerUserForm();
  await loadCustomerUsers();
}

async function changeCustomerUserPassword(userId){
  const pass = prompt('Captura la nueva contraseña para este usuario:');
  if(pass === null) return;

  const j = await apiPost('customer_user_password', {
    user_id: Number(userId || 0),
    password: String(pass || '')
  });

  if(!j.ok){
    alert(j.error || 'No se pudo actualizar la contraseña');
    return;
  }

  alert('Contraseña actualizada correctamente');
  await loadCustomerUsers();
}

async function toggleCustomerUser(userId, active){
  const ok = confirm(active === 1 ? '¿Habilitar este usuario cliente?' : '¿Deshabilitar este usuario cliente?');
  if(!ok) return;

  const j = await apiPost('customer_user_toggle', {
    user_id: Number(userId || 0),
    active: Number(active || 0)
  });

  if(!j.ok){
    alert(j.error || 'No se pudo actualizar el usuario');
    return;
  }

  await loadCustomerUsers();
}

async function loadProducts(){
  const j = await apiGet('products_admin', { q: $('productSearch').value.trim() });
  if(!j.ok){
    alert(j.error || 'No se pudieron cargar los artículos');
    return;
  }

  const rows = j.rows || [];
  const tb = $('tbProducts');
  tb.innerHTML = '';

  if(!rows.length){
    $('productsEmptyHint').textContent = 'Sin artículos para el filtro seleccionado.';
    return;
  }

  $('productsEmptyHint').textContent = '';

  rows.forEach(r => {
    const active = Number(r.active) === 1;
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="mono">${h(r.sku || '')}</td>
        <td><div class="fw-semibold">${h(r.name || '')}</div></td>
        <td class="mono">${h(r.uom || '')}</td>
        <td>${active ? '<span class="pill-active">Activo</span>' : '<span class="pill-inactive">Inactivo</span>'}</td>
        <td class="text-end">
          <button class="btn ${active ? 'btn-danger-soft' : 'btn-green-soft'} btn-sm"
                  type="button"
                  onclick="toggleProduct(${Number(r.id)}, ${active ? 0 : 1})">
            ${active ? '<i class="bi bi-slash-circle me-1"></i>Desactivar' : '<i class="bi bi-check-circle me-1"></i>Activar'}
          </button>
        </td>
      </tr>
    `);
  });
}

async function toggleProduct(productId, active){
  const ok = confirm(active === 1 ? '¿Activar este artículo?' : '¿Desactivar este artículo?');
  if(!ok) return;

  const j = await apiPost('product_toggle', {
    product_id: productId,
    active: active
  });

  if(!j.ok){
    alert(j.error || 'No se pudo actualizar el artículo');
    return;
  }

  await loadProducts();
}

document.addEventListener('DOMContentLoaded', async () => {
  detailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('detailModal'));
  inventoryModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('inventoryModal'));
  customersModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('customersModal'));
  customerUsersModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('customerUsersModal'));
  productsModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('productsModal'));
  customerEditModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('customerEditModal'));
  customerAddressesModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('customerAddressesModal'));
  dispatchModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('dispatchModal'));
  newOrderModal = new bootstrap.Modal(document.getElementById('newOrderModal'), {backdrop:'static', keyboard:false});
  newOrderConfirmModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('newOrderConfirmModal'));
  newOrderSummaryModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('newOrderSummaryModal'));
  newOrderCloseConfirmModal = new bootstrap.Modal(document.getElementById('newOrderCloseConfirmModal'), {backdrop:'static', keyboard:false});
  botellaPalletValidationModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('botellaPalletValidationModal'));
  newOrderNoStockModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('newOrderNoStockModal'));
  salesAiModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('salesAiModal'));

  await loadLocationFilters();
  await loadOrders();

  $('btnReload').addEventListener('click', loadOrders);
  $('btnSaveStatus').addEventListener('click', saveStatus);
  $('btnConfirmDispatch').addEventListener('click', confirmSalesDispatch);

  document.getElementById('newOrderModal').addEventListener('hide.bs.modal', (ev) => {
    if (!allowNewOrderClose && newOrderHasUnsavedChanges()) {
      ev.preventDefault();
      requestCloseNewOrder();
    }
  });
  document.getElementById('newOrderModal').addEventListener('hidePrevented.bs.modal', () => {
    requestCloseNewOrder();
  });
  if ($('btnNewOrderHeaderClose')) $('btnNewOrderHeaderClose').addEventListener('click', requestCloseNewOrder);
  if ($('btnNewOrderFooterClose')) $('btnNewOrderFooterClose').addEventListener('click', requestCloseNewOrder);
  if ($('btnNewOrderCloseNo')) $('btnNewOrderCloseNo').addEventListener('click', () => newOrderCloseConfirmModal.hide());
  if ($('btnNewOrderCloseYes')) $('btnNewOrderCloseYes').addEventListener('click', confirmCloseNewOrderWithoutSaving);
  $('btnNewWarehouseOrder').addEventListener('click', openNewWarehouseOrder);
  $('btnNewOrderRefreshStock').addEventListener('click', loadNewOrderStock);
  $('btnNewOrderAddItem').addEventListener('click', addNewOrderItem);
  $('newOrderType').addEventListener('change', updateNewOrderMode);
  $('newOrderVehicleUnit').addEventListener('change', updateVehicleCapacity);
    $('newOrderRoute').addEventListener('change', async () => { syncRouteLabel(); await loadNewOrderRouteStock(); updateNewOrderStockInfo(); });
  $('btnNewOrderSave').addEventListener('click', saveNewWarehouseOrder);
  $('btnNewOrderConfirmYes').addEventListener('click', executeNewWarehouseOrderSave);
  $('btnNewOrderSummaryFinish').addEventListener('click', finishNewOrderSummary);
  $('newOrderLocation').addEventListener('change', async () => { newOrderRouteStockRows=[]; await Promise.all([loadNewOrderStock(), loadNewOrderCustomers(), loadNewOrderRoutes()]); renderNewOrderRouteStock(); });
  $('newOrderCustomerId').addEventListener('change', () => { loadNewOrderCustomerAddresses(); updateNewOrderOcRelationUi(); });
  if ($('newOrderOcRelation')) $('newOrderOcRelation').addEventListener('change', updateNewOrderOcRelationUi);
  if ($('newOrderItemPurchaseOrder')) { $('newOrderItemPurchaseOrder').addEventListener('change', handleNewOrderItemOcChange); $('newOrderItemPurchaseOrder').addEventListener('input', updateOcAttachmentFieldsVisibility); }
  if ($('newOrderItemCustomerId')) $('newOrderItemCustomerId').addEventListener('change', () => loadNewOrderItemAddresses());
  if ($('newOrderItemAddressId')) $('newOrderItemAddressId').addEventListener('change', syncNewOrderItemAddressFromSelect);
  if ($('newOrderAddressId')) $('newOrderAddressId').addEventListener('change', syncNewOrderAddressFromSelect);
  $('newOrderProduct').addEventListener('change', async () => { await loadNewOrderPackaging(); warnIfSelectedProductHasNoStock(); });
  $('newOrderPackaging').addEventListener('change', () => { updateNewOrderStockInfo(); updateNewOrderCalcPreview(); });
  if ($('newOrderPalletsQty')) $('newOrderPalletsQty').addEventListener('input', updateNewOrderCalcPreview);
  document.querySelectorAll('[data-mail-folder]').forEach(btn => {
    btn.addEventListener('click', () => {
      mailboxFolder = btn.dataset.mailFolder || 'all';
      document.querySelectorAll('[data-mail-folder]').forEach(x => x.classList.toggle('active', x === btn));
      renderOrders();
    });
  });
  $('mailItems').addEventListener('click', e => {
    const item = e.target.closest('[data-mail-index]');
    if (item) selectMailboxOrder(parseInt(item.dataset.mailIndex || '0', 10));
  });
  $('btnReloadInsights').addEventListener('click', loadSalesInsights);
  $('btnOpenSalesAi').addEventListener('click', async () => { salesAiModal.show(); await loadSalesInsights(); });

  $('btnOpenInventory').addEventListener('click', async () => {
    inventoryModal.show();
    if ($('invLocationId').value) {
      await loadInventoryByLocation();
    }
  });

  $('btnOpenCustomers').addEventListener('click', async () => {
    customersModal.show();
    await loadCustomers();
  });

  $('btnOpenProducts').addEventListener('click', async () => {
    productsModal.show();
    await loadProducts();
  });

  $('btnNewCustomer').addEventListener('click', () => openCustomerEdit(''));
  $('btnCustomerSave').addEventListener('click', saveCustomer);
  $('btnCustomerAddressSave').addEventListener('click', saveCustomerAddress);
  $('btnCustomerAddressClear').addEventListener('click', clearCustomerAddressForm);

  $('btnCustomerUserSave').addEventListener('click', saveCustomerUser);
  $('btnCustomerUserClear').addEventListener('click', clearCustomerUserForm);

  $('btnLoadInventory').addEventListener('click', loadInventoryByLocation);
  $('invLocationId').addEventListener('change', loadInventoryByLocation);

  $('fStatus').addEventListener('change', loadOrders);
  $('fDateFrom').addEventListener('change', loadOrders);
  $('fDateTo').addEventListener('change', loadOrders);
  $('fLocation').addEventListener('change', async () => {
    await loadOrders();
    if (document.getElementById('customersModal')?.classList.contains('show')) {
      await loadCustomers();
    }
  });

  let t = null;
  $('fCustomer').addEventListener('input', () => {
    clearTimeout(t);
    t = setTimeout(loadOrders, 350);
  });

  $('customerSearch').addEventListener('input', () => {
    clearTimeout(customerTimer);
    customerTimer = setTimeout(loadCustomers, 300);
  });

  $('productSearch').addEventListener('input', () => {
    clearTimeout(productTimer);
    productTimer = setTimeout(loadProducts, 300);
  });
});
</script>
</body>
</html>
