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

declare(strict_types=1);

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


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

require_scope();
ensure_session();

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

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

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

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

/*
 * Alcance funcional por planta:
 * Hielo Barra conserva integraciones y ayudas específicas de barra/VentasHielo.
 * El resto de plantas trabajan como MES general por almacenes activos de su localidad.
 */
$scopePlantText = mb_strtoupper(trim($scopeCompanyName . ' ' . $scopeLocationName), 'UTF-8');
$isHieloBarraScope = (
    str_contains($scopePlantText, 'HIELO')
    && str_contains($scopePlantText, 'BARRA')
);
$scopeWarehouseHelpText = $isHieloBarraScope
    ? 'Inventario disponible Hielo Barra: BARRA = CF1 + CF2; producto terminado = CF3. CUB y SOB-CUB se muestran en detalle sin sumarse al disponible comercial.'
    : 'Inventario operativo MES: se muestran existencias por los almacenes activos de la planta actual.';

$canWrite = has_role('admin') || has_role('warehouse') || has_role('almacen');
$action   = (string)($_GET['action'] ?? '');

ensure_warehouse_merma_schema($pdo);
ensure_location_request_schema($pdo);
ensure_default_merma_concepts($pdo, $companyId);

/*
 * VentasHielo es una integración exclusiva del flujo Hielo Barra.
 * En Botella, Hielo Bolsa, Garrafón u otra planta no se ejecutan endpoints
 * ni procesos del bridge aunque se intente invocar la acción manualmente.
 */
if ($isHieloBarraScope) {
    handle_ventashielo_bridge_actions($pdo, $u, $companyId, $locationId, $canWrite, $action);
} elseif (str_starts_with(strtolower($action), 'vh_')) {
    jexit(['ok'=>false, 'error'=>'VentasHielo solo está disponible para la planta Hielo Barra.'], 403);
}

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

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

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

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

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

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

function breport_operational_start_hour(): int {
    return 7;
}

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

    return null;
}

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return $rows;
}

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

function get_product_total_available(PDO $pdo, int $companyId, int $locationId, int $productId): float {
    $st = $pdo->prepare("\n        SELECT 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 product_id = ?
    ");
    $st->execute([$companyId, $locationId, $productId]);
    return (float)($st->fetchColumn() ?: 0.0);
}

function outbound_source_warehouses(PDO $pdo, int $companyId, int $locationId, int $productId): array {
    $product = get_product_row($pdo, $companyId, $productId);
    if (!$product) return [];

    $sku = strtoupper(trim((string)($product['sku'] ?? '')));

    /*
     * Hielo Barra:
     * - Barra comercial sale únicamente de CF1/CF2.
     * - Producto terminado sale únicamente de CF3.
     * - CUB y SOB-CUB son internos y no se usan para surtir ventas/salidas.
     */
    if (breport_is_hielo_barra_scope()) {
        $codes = $sku === 'BARRA 150 KILOS' ? ['CF1','CF2'] : ['CF3'];
        $rows = [];
        foreach ($codes as $code) {
            $wid = get_warehouse_id_by_code($pdo, $companyId, $locationId, $code);
            if ($wid > 0) $rows[] = ['warehouse_id'=>$wid, 'code'=>$code, 'name'=>$code];
        }
        return $rows;
    }

    /*
     * Resto de plantas:
     * usa regla configurada por producto/localidad y, si aún no existe,
     * los almacenes activos operativos de la planta actual.
     */
    return rule_warehouses_for_product_or_all($pdo, $companyId, $locationId, $productId, 'source', true);
}

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

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

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

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

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

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

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

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

    return $alloc;
}


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

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

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

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

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

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

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

        $barPhysical += $qty;

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

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

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

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

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

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

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

    return $code === 'CF3';
}

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

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

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

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

        foreach ($whMap as $w) {
            $rows[$pid]['warehouse_breakdown'][(int)$w['id']] = [
                'warehouse_id' => (int)$w['id'],
                'code' => (string)$w['code'],
                'name' => (string)$w['name'],
                'qty' => 0.0,
                'initial' => 0.0,
                'qty_in' => 0.0,
                'qty_out' => 0.0,
            ];
        }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            $wid = (int)($r['warehouse_id'] ?? 0);
            $typ = strtoupper((string)$r['move_type']);
            $qty = (float)$r['qty'];

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

    if ($action === 'locreq_ship') {
        if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso.'], 403);
        $d = read_json();
        $id = (int)arr_get($d, 'id', 0);
        $workDate = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate = breport_resolve_work_date($workDate);
        $driverName = trim((string)arr_get($d, 'driver_name', ''));
        $vehicleUnit = trim((string)arr_get($d, 'vehicle_unit', ''));
        $shipItems = arr_get($d, 'items', []);
        if ($id <= 0) jexit(['ok'=>false,'error'=>'id requerido'], 400);
        if (!is_array($shipItems) || !$shipItems) jexit(['ok'=>false,'error'=>'Artículos de envío requeridos.'], 400);

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

        $where = ["im.company_id = ?", "im.location_id = ?", "im.product_id = ?", "im.work_date BETWEEN ? AND ?"];
        $params = [$companyId, $locationId, $productId, $dateFrom, $dateTo];
        if ($direction === 'IN') {
            $where[] = "UPPER(im.move_type) IN ('IN','ADJ')";
        } elseif ($direction === 'OUT') {
            $where[] = "UPPER(im.move_type) = 'OUT'";
        } else {
            $direction = 'ALL';
        }
        if ($isHieloBarraScope) {
            // En Hielo Barra el historial comercial de barra conserva CF1 + CF2.
            $where[] = "(UPPER(p.sku) <> 'BARRA 150 KILOS' OR UPPER(COALESCE(w.code,'')) IN ('CF1','CF2'))";
        }
        $whereSql = implode(' AND ', $where);

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

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


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

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

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

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


    /*
     * BANDEJA OPERATIVA DE SALIDAS
     * ------------------------------------------------------------
     * Consulta solamente:
     * - Ordenes de ventas creadas en BReport.
     * - Salidas manuales generadas desde almacén.
     * VentasHielo continúa consumiéndose por su API existente
     * vh_authorized / vh_order_detail para no alterar el bridge.
     */
    if ($action === 'dispatch_mailbox_sales') {
        $date = trim((string)($_GET['date'] ?? ''));
        $where = [
            "o.company_id = ?",
            "o.location_id = ?",
            "(UPPER(COALESCE(o.source,'')) = 'VENTAS' OR o.sales_order_ref LIKE 'OSV-%')",
            "UPPER(COALESCE(o.status,'')) NOT IN ('CANCELADA','CANCELADO','ELIMINADA','ELIMINADO')"
        ];
        $params = [$companyId, $locationId];

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

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

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

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

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

        $st = $pdo->prepare("
            SELECT
                ob.id,
                ob.outbound_date,
                ob.reference,
                ob.status,
                ob.notes,
                oc.name AS channel_name,
                COALESCE(u.full_name, u.username, CONCAT('Usuario #', ob.created_by)) AS user_name,
                COUNT(oi.product_id) AS items_count,
                COALESCE(SUM(oi.qty),0) AS total_qty
            FROM outbounds ob
            LEFT JOIN outbound_channels oc ON oc.id = ob.channel_id
            LEFT JOIN users u ON u.id = ob.created_by
            LEFT JOIN outbound_items oi ON oi.outbound_id = ob.id
            WHERE " . implode(' AND ', $where) . "
            GROUP BY
                ob.id, ob.outbound_date, ob.reference, ob.status, ob.notes,
                oc.name, u.full_name, u.username, ob.created_by
            ORDER BY ob.outbound_date DESC, ob.id DESC
            LIMIT 300
        ");
        $st->execute($params);
        jexit(['ok'=>true, 'rows'=>$st->fetchAll(PDO::FETCH_ASSOC), 'date'=>$date]);
    }

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            $notesExtra = [];
            $notesExtra[] = 'Pedido ventas #' . $orderId;
            if (!empty($order['sales_order_ref'])) $notesExtra[] = 'Folio ventas: ' . $order['sales_order_ref'];
            if (!empty($order['business_name'])) $notesExtra[] = 'Cliente/Destino: ' . $order['business_name'];
            if ($driverName !== '') $notesExtra[] = 'Chofer: ' . $driverName;
            if ($vehicle !== '') $notesExtra[] = 'Unidad: ' . $vehicle;
            if ($departAt !== '') $notesExtra[] = 'Salida: ' . $departAt;
            if ($notes !== '') $notesExtra[] = 'Notas: ' . $notes;

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

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

            $stItem = $pdo->prepare("
                INSERT INTO outbound_items (outbound_id, product_id, qty)
                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()
                )
            ");

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

                foreach (allocate_inventory_out($pdo, $companyId, $locationId, $productIdMove, $qtyMove) as $alloc) {
                    $stMove->execute([
                        $companyId,
                        $locationId,
                        $alloc['warehouse_id'],
                        $workDate,
                        $productIdMove,
                        (float)$alloc['qty'],
                        $channelId,
                        $outboundId,
                        $fullNotes,
                        (int)$u['id']
                    ]);
                }
            }

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

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

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

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

        $d = read_json();

        $workDate   = trim((string)arr_get($d, 'work_date', breport_operational_today()));
        $workDate = breport_resolve_work_date($workDate);
        $channelId  = (int)arr_get($d, 'channel_id', 0);
        $reference  = trim((string)arr_get($d, 'reference', ''));
        $notes      = trim((string)arr_get($d, 'notes', ''));
        $items      = arr_get($d, 'items', []);
        $driverName = trim((string)arr_get($d, 'driver_name', ''));
        $vehicle    = trim((string)arr_get($d, 'vehicle_unit', ''));
        $customer   = trim((string)arr_get($d, 'customer_name', ''));
        $departAt   = trim((string)arr_get($d, 'depart_at', ''));

        if ($channelId <= 0) {
            jexit(['ok' => false, 'error' => 'Canal requerido.'], 400);
        }
        if (!is_array($items) || !$items) {
            jexit(['ok' => false, 'error' => 'Debes agregar al menos un artículo.'], 400);
        }

        $cleanItems = [];
        foreach ($items as $it) {
            $pid = (int)($it['product_id'] ?? 0);
            $qty = (float)($it['qty'] ?? 0);
            if ($pid > 0 && $qty > 0) {
                $cleanItems[] = ['product_id' => $pid, 'qty' => $qty];
            }
        }

        if (!$cleanItems) {
            jexit(['ok' => false, 'error' => 'Artículos inválidos.'], 400);
        }

        foreach ($cleanItems as $it) {
            $available = get_product_outbound_available($pdo, $companyId, $locationId, (int)$it['product_id']);
            if ($it['qty'] > $available) {
                jexit(['ok' => false, 'error' => 'Inventario insuficiente para uno de los productos.'], 409);
            }
        }

        $notesExtra = [];
        if ($customer !== '')   $notesExtra[] = 'Cliente/Destino: ' . $customer;
        if ($driverName !== '') $notesExtra[] = 'Chofer: ' . $driverName;
        if ($vehicle !== '')    $notesExtra[] = 'Unidad: ' . $vehicle;
        if ($departAt !== '')   $notesExtra[] = 'Salida: ' . $departAt;
        if ($notes !== '')      $notesExtra[] = 'Notas: ' . $notes;

        $fullNotes = implode(' | ', $notesExtra);
        if ($fullNotes === '') $fullNotes = null;
        if ($reference === '') $reference = null;

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

            $stItem = $pdo->prepare("
                INSERT INTO outbound_items (outbound_id, product_id, qty)
                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()
                )
            ");

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

                foreach (allocate_inventory_out($pdo, $companyId, $locationId, $productIdMove, $qtyMove) as $alloc) {
                    $stMove->execute([
                        $companyId,
                        $locationId,
                        $alloc['warehouse_id'],
                        $workDate,
                        $productIdMove,
                        (float)$alloc['qty'],
                        $channelId,
                        $outboundId,
                        $fullNotes,
                        (int)$u['id']
                    ]);
                }
            }

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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        $d = read_json();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  html{
    color-scheme:light;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  #emptyHint{ color:#b7cae8!important; }

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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



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



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


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

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

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

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

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

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

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

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

          <div class="d-flex gap-2 flex-wrap">
            <button id="btnReload" class="btn toolbar-btn" type="button">
              <i class="bi bi-arrow-repeat me-1"></i>Recargar
            </button>
            <button id="btnMerma" class="btn btn-gold-soft" type="button" <?= !$canWrite ? 'disabled' : '' ?>>
              <i class="bi bi-exclamation-triangle me-1"></i>Registrar merma
            </button>
            <button id="btnTransfer" class="btn btn-blue-soft" type="button" <?= !$canWrite ? 'disabled' : '' ?>>
              <i class="bi bi-arrow-left-right me-1"></i>Transferir almacén
            </button>
            <button id="btnAdd" class="btn btn-green-soft" type="button" <?= !$canWrite ? 'disabled' : '' ?>>
              <i class="bi bi-truck me-1"></i>Registrar salida
            </button>
            <button id="btnReturn" class="btn btn-blue-soft" type="button" <?= !$canWrite ? 'disabled' : '' ?>>
              <i class="bi bi-arrow-counterclockwise me-1"></i>Registrar devolución
            </button>
          </div>
        </div>

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

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

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

        <div class="row g-3 mt-2" id="productCardsRow"></div>
      </div>
      <div class="operation-nav glass mb-4">
        <div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
          <div>
            <div class="fw-bold">Centro operativo de almacén</div>
            <div class="small muted">Revisa inventario, salidas y movimientos sin duplicar información.</div>
          </div>
          <div class="d-flex flex-wrap gap-2">
            <button type="button" class="operation-nav-btn active" data-ops-section="dispatchMailboxSection">
              <i class="bi bi-inboxes-fill"></i> Bandeja de salidas
            </button>
            <button type="button" class="operation-nav-btn" data-ops-section="inventorySection">
              <i class="bi bi-box-seam"></i> Inventario
            </button>
            <button type="button" class="operation-nav-btn" data-ops-section="locReqSection">
              <i class="bi bi-building-up"></i> Solicitudes
            </button>
            <button type="button" class="operation-nav-btn" data-ops-section="returnSection">
              <i class="bi bi-arrow-counterclockwise"></i> Devoluciones
            </button>
          </div>
        </div>
      </div>

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

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

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

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

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

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

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

      </div>
      </div>

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

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

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

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


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

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

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

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

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

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

    </div>
  </div>

  <div class="modal fade" id="editModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
      <form class="modal-content" id="editForm">
        <div class="modal-header">
          <div>
            <div class="fw-bold fs-5">Registrar salida de almacén</div>
            <div class="muted small">Preparado para logística y flujo futuro PWA.</div>
          </div>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
        </div>

        <div class="modal-body">
          <div class="row g-3">
            <div class="col-12 col-md-4">
              <label class="form-label">Fecha</label>
              <input class="form-control" type="date" name="work_date" required>
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Canal</label>
              <select class="form-select" name="channel_id" id="fChannel" required></select>
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Referencia</label>
              <input class="form-control" name="reference" placeholder="Folio, ticket, embarque">
            </div>

            <div class="col-12 col-md-4">
              <label class="form-label">Cliente / destino</label>
              <input class="form-control" name="customer_name" placeholder="Cliente, ruta, destino">
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Chofer</label>
              <input class="form-control" name="driver_name" placeholder="Nombre del chofer">
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Unidad / transporte</label>
              <input class="form-control" name="vehicle_unit" placeholder="Camión, placa, unidad">
            </div>

            <div class="col-12">
              <label class="form-label">Hora salida</label>
              <input class="form-control" type="datetime-local" name="depart_at">
            </div>

            <div class="col-12">
              <label class="form-label">Notas</label>
              <input class="form-control" name="notes" placeholder="Observaciones">
            </div>
          </div>

          <hr class="my-4">

          <div class="d-flex align-items-center justify-content-between mb-3">
            <div class="section-title">
              <i class="bi bi-boxes text-warning"></i>
              Artículos de salida
            </div>
            <button class="btn toolbar-btn" type="button" id="btnAddItem">
              <i class="bi bi-plus-lg me-1"></i>Agregar artículo
            </button>
          </div>

          <div id="itemsWrap" class="d-grid gap-3"></div>
        </div>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

              <div class="col-12 col-md-4">
                <label class="form-label">Chofer</label>
                <input id="sdDriverName" class="form-control" placeholder="Nombre del chofer">
              </div>

              <div class="col-12 col-md-4">
                <label class="form-label">Unidad / transporte</label>
                <input id="sdVehicleUnit" class="form-control" placeholder="Camión, placa, unidad">
              </div>

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

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

        <div class="modal-footer">
          <button class="btn toolbar-btn" type="button" data-bs-dismiss="modal">Cerrar</button>
          <button id="btnDispatchSalesOrder" class="btn btn-gold-soft" type="button">
            <i class="bi bi-box-arrow-up-right me-1"></i>Generar salida y PDF
          </button>
        </div>
      </div>
    </div>
  </div>

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

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


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

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


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

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


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

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

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

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

  const r = await fetch(u.toString(), {
    credentials:'same-origin',
    headers:{'X-CSRF': CSRF}
  });
  const txt = await r.text();
  try { 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'};
  }
}

let modal, detailModal, transferModal, returnModal, returnConfirmModal, returnResultModal, salesDetailModal, routeOrdersModal, confirmDispatchModal, dispatchResultModal, locReqModal, locReqDetailModal, locReqConfirmModal, locReqRejectModal, movementHistoryModal;
let pendingWarehouseReturnPayload = null;
let products = [];
let channels = [];
let warehouses = [];
let invRows = [];
let barProductId = 0;
let salesRows = [];
let salesCreatedRows = [];
let salesRouteGroups = [];
let selectedRouteGroup = null;
let salesPage = 1;
let salesPageSize = 10;
let salesPages = 1;
let salesTotal = 0;
let currentSalesOrder = null;
let mermaRows = [];
let mermaPage = 1;
let mermaPageSize = 10;
let mermaPagination = {};
let locReqRows = [];
let locReqMode = 'received';
let locReqLocations = [];
let historySummaryRows = [];
let movementHistoryState = {product_id:0, sku:'', product_name:'', direction:'ALL', date_from:'', date_to:'', page:1, per_page:25};
let systemTimeInfo = null;
let operationalAutoRefreshRunning = false;

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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  if (!rows.length) {
    $('emptyHint').textContent = 'Sin movimientos registrados.';
    return;
  }
  $('emptyHint').textContent = '';

  for (const r of rows) {
    const typ = r.move_type === 'OUT' ? 'Salida' : (r.move_type === 'IN' ? 'Entrada' : 'Ajuste');
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td><div class="mono fw-semibold">${esc(r.work_date || '')}</div><div class="small muted mono">${esc(String(r.created_at || '').substring(11,19))}</div></td>
        <td>
          <div class="fw-semibold" >${esc(r.sku)}</div>
          <div class="small" >${esc(r.product_name || '')}</div>
        </td>
        <td>${esc(typ)}</td>
        <td>
          <div class="mono fw-semibold">${esc(r.warehouse_code || '-')}</div>
          <div class="small muted">${esc(r.warehouse_name || '')}</div>
        </td>
        <td>
          <div class="mono">${esc(r.ref_type || r.channel_name || '-')}</div>
          <div class="small muted">${r.ref_id ? ('#' + esc(r.ref_id)) : ''}</div>
        </td>
        <td>
          <div class="small fw-semibold">${esc(r.user_name || r.username || '-')}</div>
          <div class="small muted mono">${esc(r.username || '')}</div>
        </td>
        <td class="text-end mono fw-bold">${fmt(r.qty)}</td>
        <td class="small" >${esc(r.notes || '')}</td>
        <td class="text-end">
          <span class="small muted">Histórico</span>
        </td>
      </tr>
    `);
  }
}

function renderSalesOrders(){
  const tb = $('salesBody');
  tb.innerHTML = '';

  if (!salesRows.length) {
    $('salesEmptyHint').textContent = 'Sin pedidos autorizada de VentasHielo pendientes de surtir.';
    return;
  }

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

  salesRows.forEach(r => {
    const folio = r.sales_order || r.ov_system || ('PW-' + r.id);
    const fecha = r.delivery_date || '';
    const hora = r.delivery_time || '';
    const cliente = r.cliente || '';
    const destino = r.address_label && r.address_label !== '—' ? r.address_label : (r.delivery_address || '');
    const statusTxt = String(r.status || 'autorizada').toUpperCase();

    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="mono fw-bold">${esc(folio)}</td>
        <td>
          <div>${esc(fecha)}</div>
          <div class="small">${esc(hora)}</div>
        </td>
        <td>
          <div class="fw-semibold" >${esc(cliente)}</div>
          <div class="small" >${esc(destino)}</div>
        </td>
        <td class="mono text-center">${esc(r.items_count || 0)}</td>
        <td class="text-center mono fw-bold">${esc(r.Qty_pza || '')}</td>
        <td><span class="status-chip status-autorizada">${esc(statusTxt)}</span></td>
        <td class="text-end">
          <button class="btn btn-gold-soft btn-sm" type="button" data-sales-detail="${r.id}" ${CAN_WRITE ? '' : 'disabled'}>
            <i class="bi bi-eye me-1"></i>Ver / Surtir
          </button>
        </td>
      </tr>
    `);
  });
}


function routeLabel(routeNo){
  const n = parseInt(routeNo || 0, 10);
  return n > 0 ? `Ruta ${n}` : 'Sin ruta';
}
function routeStatusInfo(group){
  const pending = parseInt(group.pending_count || 0, 10);
  const dispatched = parseInt(group.dispatched_count || 0, 10);
  if (pending > 0 && dispatched > 0) return {cls:'mixed', txt:'Parcial', icon:'bi-exclamation-circle'};
  if (pending > 0) return {cls:'pending', txt:'Pendiente de salida', icon:'bi-x-circle'};
  return {cls:'done', txt:'Salida generada', icon:'bi-check2-circle'};
}
function renderSalesRouteCards(){
  const wrap = $('salesRouteCards');
  if (!wrap) return;
  wrap.innerHTML = '';
  if (!salesRouteGroups.length) {
    $('salesRouteEmptyHint').textContent = 'Sin pedidos de ruta para la fecha seleccionada.';
    wrap.innerHTML = `<div class="col-12"><div class="route-empty"><i class="bi bi-calendar-x fs-3 d-block mb-2"></i>No hay rutas activas para esta fecha.</div></div>`;
    return;
  }
  $('salesRouteEmptyHint').textContent = '';
  salesRouteGroups.forEach(g => {
    const st = routeStatusInfo(g);
    const routeNo = parseInt(g.route_no || 0, 10);
    wrap.insertAdjacentHTML('beforeend', `
      <div class="col-12 col-md-6 col-xl-4">
        <button class="route-card route-${st.cls}" type="button" data-route-card="${esc(routeNo)}">
          <div class="d-flex justify-content-between align-items-start gap-2 mb-3">
            <div><div class="route-meta">Ruta de entrega</div><div class="route-number">${esc(routeLabel(routeNo))}</div></div>
            <span class="route-badge ${st.cls}"><i class="bi ${st.icon}"></i>${esc(st.txt)}</span>
          </div>
          <div class="row g-2">
            <div class="col-6"><div class="route-meta">Pedidos</div><div class="fw-bold mono">${esc(g.orders_count || 0)}</div></div>
            <div class="col-6"><div class="route-meta">Artículos</div><div class="fw-bold mono">${esc(g.items_count || 0)}</div></div>
            <div class="col-6"><div class="route-meta">Pendientes</div><div class="fw-bold mono text-danger">${esc(g.pending_count || 0)}</div></div>
            <div class="col-6"><div class="route-meta">Surtidos</div><div class="fw-bold mono text-success">${esc(g.dispatched_count || 0)}</div></div>
          </div>
          <div class="route-meta mt-3">Cantidad total: <b class="mono">${fmt(g.total_qty || 0)}</b></div>
        </button>
      </div>
    `);
  });
}
function openRouteOrders(routeNo){
  const rn = parseInt(routeNo || 0, 10);
  selectedRouteGroup = salesRouteGroups.find(g => parseInt(g.route_no || 0, 10) === rn) || null;
  if (!selectedRouteGroup) return;
  const st = routeStatusInfo(selectedRouteGroup);
  $('routeOrdersTitle').textContent = `${routeLabel(rn)} · ${st.txt}`;
  $('routeOrdersSub').textContent = `Fecha ${$('salesRouteDate').value || ''} · ${selectedRouteGroup.orders_count || 0} pedido(s)`;
  const list = $('routeOrdersList');
  list.innerHTML = '';
  (selectedRouteGroup.orders || []).forEach(o => {
    const folio = o.sales_order_ref || ('OSV-' + String(o.id).padStart(6, '0'));
    const done = !!o.outbound_id || String(o.status || '').toUpperCase() !== 'AUTORIZADA';
    const cls = done ? 'done' : 'pending';
    const statusTxt = done ? 'Salida generada' : 'Pendiente de salida';
    const pdfUrl = `./orden_entrega_pdf.php?order_id=${encodeURIComponent(o.id)}&outbound_id=${encodeURIComponent(o.outbound_id || '')}&folio=${encodeURIComponent(folio)}`;
    list.insertAdjacentHTML('beforeend', `
      <div class="col-12 col-lg-6">
        <div class="route-order-card ${cls}">
          <div class="d-flex justify-content-between align-items-start gap-2 mb-2">
            <div><div class="fw-bold mono fs-5">${esc(folio)}</div><div class="small muted">${esc(o.requested_delivery_date || '')} ${esc(o.requested_delivery_time || '')}</div></div>
            <span class="route-badge ${done ? 'done' : 'pending'}"><i class="bi ${done ? 'bi-check2-circle' : 'bi-x-circle'}"></i>${statusTxt}</span>
          </div>
          <div class="fw-semibold">${esc(o.business_name || '')}</div>
          <div class="small muted mb-2">${esc(o.delivery_address || '')}</div>
          <div class="row g-2 mb-3">
            <div class="col-4"><div class="route-meta">Items</div><div class="fw-bold mono">${esc(o.items_count || 0)}</div></div>
            <div class="col-4"><div class="route-meta">Cantidad</div><div class="fw-bold mono">${fmt(o.total_qty || 0)}</div></div>
            <div class="col-4"><div class="route-meta">Salida</div><div class="fw-bold mono">${esc(o.outbound_id || '-')}</div></div>
          </div>
          <div class="d-flex gap-2 flex-wrap justify-content-end">
            ${done ? `<a class="btn btn-green-soft btn-sm" href="${pdfUrl}" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</a>` : ''}
            <button class="btn ${done ? 'btn-blue-soft' : 'btn-gold-soft'} btn-sm" type="button" data-breport-sales-detail="${esc(o.id)}" ${CAN_WRITE || done ? '' : 'disabled'}>
              <i class="bi bi-eye me-1"></i>${done ? 'Ver detalle' : 'Ver / Generar salida'}
            </button>
          </div>
        </div>
      </div>
    `);
  });
  routeOrdersModal.show();
}

function dispatchStatusLabel(folder, row){
  if (folder === 'warehouse') return {text:'GENERADA', cls:'manual'};
  const done = row.outbound_id || String(row.status || '').toUpperCase() !== 'AUTORIZADA';
  return done ? {text:'SURTIDA', cls:'done'} : {text:'POR SURTIR', cls:'pending'};
}

function dispatchRowDate(folder, row){
  if (folder === 'vh') return row.delivery_date || '';
  if (folder === 'sales') return row.requested_delivery_date || '';
  return row.outbound_date || '';
}

function dispatchRowFolio(folder, row){
  if (folder === 'vh') return row.sales_order || row.ov_system || ('PW-' + row.id);
  if (folder === 'sales') return row.sales_order_ref || ('OSV-' + row.id);
  return row.reference || ('SAL-' + row.id);
}

function dispatchRowCustomer(folder, row){
  if (folder === 'vh') return row.cliente || 'Cliente VentasHielo';
  if (folder === 'sales') return row.business_name || row.contact_name || 'Cliente';
  return row.channel_name || 'Salida manual de almacén';
}

function renderDispatchMailboxList(){
  const box = $('dispatchMailboxItems');
  if (!box) return;
  const folder = dispatchMailboxFolder;
  const rows = dispatchMailboxRows[folder] || [];
  const titles = {
    vh: ['VentasHielo · por surtir', 'Pedidos autorizados pendientes de generar salida'],
    sales: ['Salidas de Ventas', 'Órdenes de venta de la planta seleccionada'],
    warehouse: ['Salidas de Almacén', 'Movimientos capturados directamente por almacén']
  };
  $('dispatchFolderTitle').textContent = titles[folder][0];
  $('dispatchFolderSub').textContent = `${rows.length} registro(s) · ${dispatchMailboxAllDates ? 'todos los días' : ($('dispatchMailboxDate')?.value || 'día seleccionado')}`;

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

  box.innerHTML = rows.map((r, idx) => {
    const status = dispatchStatusLabel(folder, r);
    const folio = dispatchRowFolio(folder, r);
    const customer = dispatchRowCustomer(folder, r);
    const date = dispatchRowDate(folder, r);
    const qty = fmt(r.total_qty ?? r.Qty_pza ?? 0);
    return `
      <button type="button" class="dispatch-item ${idx === 0 ? 'active' : ''}" data-dispatch-index="${idx}">
        <div class="d-flex justify-content-between align-items-start gap-2">
          <div class="dispatch-item-title mono">${esc(folio)}</div>
          <span class="dispatch-status ${status.cls}">${esc(status.text)}</span>
        </div>
        <div class="dispatch-item-sub">${esc(customer)}</div>
        <div class="dispatch-item-bottom">
          <span class="mono">${esc(date || 'Sin fecha')}</span>
          <span class="mono fw-bold">${esc(qty)} pz</span>
        </div>
      </button>
    `;
  }).join('');

  selectDispatchMailboxRow(0);
}

async function selectDispatchMailboxRow(index){
  const folder = dispatchMailboxFolder;
  const rows = dispatchMailboxRows[folder] || [];
  const row = rows[index];
  if (!row) return;

  document.querySelectorAll('#dispatchMailboxItems .dispatch-item').forEach((el, i) => {
    el.classList.toggle('active', i === index);
  });

  dispatchMailboxSelected = {folder, row};
  const status = dispatchStatusLabel(folder, row);
  const folio = dispatchRowFolio(folder, row);
  const customer = dispatchRowCustomer(folder, row);
  const qty = fmt(row.total_qty ?? row.Qty_pza ?? 0);
  let itemsHtml = '';
  let actionHtml = '';

  if (folder === 'warehouse') {
    const detail = await apiGet('dispatch_mailbox_outbound_detail', {outbound_id: row.id});
    if (!detail.ok) {
      $('dispatchMailboxDetail').innerHTML = `<div class="dispatch-empty">No fue posible consultar el detalle de la salida.</div>`;
      return;
    }
    itemsHtml = (detail.items || []).map(it => `
      <tr>
        <td><div class="mono fw-bold">${esc(it.sku || '')}</div><div class="small muted">${esc(it.product_name || '')}</div></td>
        <td class="text-end mono fw-bold">${fmt(it.qty)}</td>
        <td>${esc(it.uom || '')}</td>
      </tr>
    `).join('');
    actionHtml = `<button class="btn toolbar-btn" type="button" id="btnDispatchRegisterNew"><i class="bi bi-plus-lg me-1"></i>Nueva salida manual</button>`;
  } else if (folder === 'vh') {
    itemsHtml = `<tr><td colspan="3" class="small muted">Usa “Ver / surtir” para consultar artículos e inventario disponible antes de registrar la salida.</td></tr>`;
    actionHtml = `<button class="btn btn-green-soft" type="button" data-dispatch-open-vh="${esc(row.id)}"><i class="bi bi-box-arrow-right me-1"></i>Ver / surtir</button>`;
  } else {
    itemsHtml = `<tr><td colspan="3" class="small muted">Abre el detalle de la orden para consultar artículos, ruta y salida relacionada.</td></tr>`;
    actionHtml = `<button class="btn ${status.cls === 'pending' ? 'btn-green-soft' : 'btn-blue-soft'}" type="button" data-dispatch-open-sales="${esc(row.id)}"><i class="bi bi-eye me-1"></i>${status.cls === 'pending' ? 'Ver / surtir' : 'Ver detalle'}</button>`;
  }

  $('dispatchMailboxDetail').innerHTML = `
    <div class="d-flex justify-content-between gap-2 align-items-start flex-wrap mb-3">
      <div>
        <div class="small muted">Folio / referencia</div>
        <div class="fw-bold fs-5 mono">${esc(folio)}</div>
        <div class="muted small">${esc(customer)}</div>
      </div>
      <span class="dispatch-status ${status.cls}">${esc(status.text)}</span>
    </div>
    <div class="row g-2 mb-3">
      <div class="col-6">
        <div class="dispatch-detail-card h-100">
          <div class="dispatch-detail-label">Fecha</div>
          <div class="dispatch-detail-value mono">${esc(dispatchRowDate(folder, row) || '—')}</div>
        </div>
      </div>
      <div class="col-6">
        <div class="dispatch-detail-card h-100">
          <div class="dispatch-detail-label">Cantidad</div>
          <div class="dispatch-detail-value mono">${esc(qty)} pz</div>
        </div>
      </div>
      <div class="col-12">
        <div class="dispatch-detail-card">
          <div class="dispatch-detail-label">${folder === 'warehouse' ? 'Canal / usuario' : 'Destino / información'}</div>
          <div class="dispatch-detail-value">${esc(folder === 'warehouse' ? ((row.channel_name || 'Salida manual') + ' · ' + (row.user_name || '')) : (row.delivery_address || row.address_label || customer))}</div>
        </div>
      </div>
    </div>
    <div class="table-wrap mb-3">
      <table class="table table-sm align-middle mb-0">
        <thead><tr><th>Artículos / detalle</th><th class="text-end">Cantidad</th><th>UOM</th></tr></thead>
        <tbody>${itemsHtml}</tbody>
      </table>
    </div>
    <div class="d-flex gap-2 flex-wrap">${actionHtml}</div>
  `;

  if ($('btnDispatchRegisterNew')) {
    $('btnDispatchRegisterNew').addEventListener('click', () => $('btnAdd')?.click());
  }
}

async function loadDispatchMailbox(){
  const date = dispatchMailboxAllDates ? '' : ($('dispatchMailboxDate')?.value || operationalDateForForms());

  let vhRows = [];
  if (IS_HIELO_BARRA_SCOPE) {
    const vh = await apiGet('vh_authorized', {page:1, pageSize:300});
    if (vh.ok) {
      vhRows = (vh.rows || []).filter(r => date === '' || String(r.delivery_date || '') === date);
    }
  }

  const [sales, warehouse] = await Promise.all([
    apiGet('dispatch_mailbox_sales', {date}),
    apiGet('dispatch_mailbox_outbounds', {date})
  ]);

  dispatchMailboxRows.vh = vhRows;
  dispatchMailboxRows.sales = sales.ok ? (sales.rows || []) : [];
  dispatchMailboxRows.warehouse = warehouse.ok ? (warehouse.rows || []) : [];

  if ($('dispatchCountVH')) $('dispatchCountVH').textContent = String(dispatchMailboxRows.vh.length);
  if ($('dispatchCountSales')) $('dispatchCountSales').textContent = String(dispatchMailboxRows.sales.length);
  if ($('dispatchCountWarehouse')) $('dispatchCountWarehouse').textContent = String(dispatchMailboxRows.warehouse.length);

  const pendingSales = dispatchMailboxRows.sales.filter(r => !r.outbound_id && String(r.status || '').toUpperCase() === 'AUTORIZADA').length;
  const totalPending = dispatchMailboxRows.vh.length + pendingSales;
  if ($('countDispatchMailbox')) $('countDispatchMailbox').textContent = String(totalPending);

  const suggestion = $('dispatchSuggestion');
  if (suggestion) {
    if (totalPending > 0) {
      suggestion.innerHTML = `<b>Sugerencia:</b> hay ${totalPending} salida(s) pendientes de surtir. Prioriza pedidos autorizados antes de registrar nuevas salidas manuales.`;
    } else {
      suggestion.innerHTML = `<b>Bandeja al día:</b> no hay pedidos autorizados pendientes en el filtro actual.`;
    }
  }

  renderDispatchMailboxList();
}

async function loadSalesCreatedOrders(){
  const date = $('salesRouteDate')?.value || operationalDateForForms();
  const j = await apiGet('sales_route_orders', {date});
  if (!j.ok) {
    alert(j.error || 'Error al cargar rutas creadas desde ventas');
    return;
  }
  salesRouteGroups = j.routes || [];
  salesCreatedRows = j.rows || [];
  renderSalesRouteCards();
}

function updateModuleCounts(moveRows = [], mermaRowsLocal = []){
  const available = invRows.filter(x => num(x.on_hand) > 0).length;
  if ($('countInventory')) $('countInventory').textContent = String(available);
  if ($('countVH')) $('countVH').textContent = String(salesTotal || salesRows.length || 0);
  if ($('countSalesCreated')) $('countSalesCreated').textContent = String(salesCreatedRows.length || 0);
  if ($('countReturns')) $('countReturns').textContent = String((moveRows || []).filter(x => String(x.ref_type || '').toUpperCase() === 'WAREHOUSE_RETURN').length);
  if ($('countLocReq')) $('countLocReq').textContent = String((locReqRows || []).length);
}


function locReqStatusBadge(status){
  const s = String(status || '').toUpperCase();
  const cls = s === 'PENDIENTE' ? 'text-bg-warning' : s === 'ENVIADA' ? 'text-bg-info' : s === 'RECIBIDA' ? 'text-bg-success' : s === 'RECHAZADA' ? 'text-bg-danger' : 'text-bg-secondary';
  return `<span class="badge ${cls}">${esc(s)}</span>`;
}

function createLocReqItemRow(){
  const div = document.createElement('div');
  div.className = 'item-row';
  div.innerHTML = `
    <div class="row g-3 align-items-end">
      <div class="col-12 col-md-8">
        <label class="form-label">Producto</label>
        <select class="form-select lr-product" required>
          ${products.map(p => `<option value="${p.id}">${esc(p.sku)} · ${esc(p.name || '')}</option>`).join('')}
        </select>
      </div>
      <div class="col-8 col-md-3">
        <label class="form-label">Cantidad</label>
        <input class="form-control mono lr-qty" inputmode="decimal" value="0" required>
      </div>
      <div class="col-4 col-md-1 text-end">
        <button class="btn btn-outline-danger" type="button" title="Quitar" onclick="this.closest('.item-row').remove()"><i class="bi bi-x-lg"></i></button>
      </div>
    </div>`;
  return div;
}


function locReqActorText(r){
  const status = String(r?.status || '').toUpperCase();
  if (status === 'RECHAZADA') {
    return r.rejected_by_name ? `Rechazó: ${esc(r.rejected_by_name)}` : 'Rechazó: —';
  }
  if (r.approved_by_name) return `Autorizó/envió: ${esc(r.approved_by_name)}`;
  if (r.shipped_by_name) return `Envió: ${esc(r.shipped_by_name)}`;
  return 'Pendiente de autorización/envío';
}

function openLocReqConfirmModal({title='Confirmar', sub='Valida la información antes de continuar.', body='', okText='Confirmar', okClass='btn btn-blue-soft', onConfirm=null}){
  $('locReqConfirmTitle').textContent = title;
  $('locReqConfirmSub').textContent = sub;
  $('locReqConfirmBody').innerHTML = body;
  const ok = $('locReqConfirmOk');
  ok.className = okClass;
  ok.innerHTML = okText;
  ok.onclick = async () => {
    if (typeof onConfirm === 'function') await onConfirm();
  };
  locReqConfirmModal.show();
}

async function loadLocReqLocations(){
  const j = await apiGet('locreq_locations');
  locReqLocations = j.ok ? (j.rows || []) : [];
}

async function loadLocRequests(mode = locReqMode){
  locReqMode = mode || 'received';
  const j = await apiGet('locreq_list', {mode: locReqMode});
  if (!j.ok) {
    alert(j.error || 'Error al cargar solicitudes entre localidades');
    return;
  }
  locReqRows = j.rows || [];
  renderLocRequests();
  if ($('countLocReq')) $('countLocReq').textContent = String(locReqRows.length);
}

function renderLocRequests(){
  const tb = $('locReqBody');
  if (!tb) return;
  tb.innerHTML = '';
  if (!locReqRows.length) {
    $('locReqEmptyHint').textContent = 'Sin solicitudes para mostrar.';
    return;
  }
  $('locReqEmptyHint').textContent = '';
  for (const r of locReqRows) {
    const tr = document.createElement('tr');
    tr.innerHTML = `
      <td class="mono">SL-${String(r.id).padStart(6,'0')}</td>
      <td><div>${esc(r.request_location_name || '')}</div><div class="small muted">${esc(r.requested_by_name || '')}</div></td>
      <td>${esc(r.source_location_name || '')}</td>
      <td><div class="small">${locReqActorText(r)}</div>${String(r.status || '').toUpperCase()==='RECHAZADA' && r.reject_reason ? '<div class="small text-danger">Motivo: '+esc(r.reject_reason)+'</div>' : ''}</td>
      <td>${locReqStatusBadge(r.status)}</td>
      <td><div class="small">${esc(r.item_summary || '')}</div></td>
      <td><div class="small">${esc(r.driver_name || '-')} / ${esc(r.vehicle_unit || '-')}</div></td>
      <td><div class="small">${esc(r.request_date || '')}${r.required_date ? '<br><span class="muted">Req: '+esc(r.required_date)+'</span>' : ''}</div></td>
      <td class="text-end"><button class="btn toolbar-btn btn-sm" type="button" data-locreq-detail="${r.id}"><i class="bi bi-eye me-1"></i>Ver</button></td>
    `;
    tb.appendChild(tr);
  }
}

async function openLocReqModal(){
  await loadLocReqLocations();
  const sel = $('lrSourceLocation');
  sel.innerHTML = locReqLocations.map(l => `<option value="${l.id}">${esc(l.name)} ${l.code ? '('+esc(l.code)+')' : ''}</option>`).join('');
  const wrap = $('locReqItemsWrap');
  wrap.innerHTML = '';
  wrap.appendChild(createLocReqItemRow());
  $('locReqForm').reset();
  if (!locReqLocations.length) {
    alert('No hay otra localidad activa en esta empresa para solicitar producto.');
    return;
  }
  locReqModal.show();
}

async function saveLocReq(){
  const f = $('locReqForm');
  const items = [...document.querySelectorAll('#locReqItemsWrap .item-row')].map(row => ({
    product_id: parseInt(row.querySelector('.lr-product')?.value || '0', 10),
    qty: num(row.querySelector('.lr-qty')?.value || 0)
  })).filter(x => x.product_id > 0 && x.qty > 0);
  if (!items.length) { alert('Agrega al menos un artículo con cantidad mayor a cero.'); return; }
  const payload = {
    source_location_id: parseInt(f.source_location_id.value || '0', 10),
    required_date: f.required_date.value || '',
    notes: f.notes.value || '',
    items
  };
  const j = await apiPost('locreq_create', payload);
  if (!j.ok) { alert(j.error || 'No se pudo crear la solicitud'); return; }
  locReqModal.hide();
  await loadLocRequests('created');
  showModuleSection('locReqSection');
}

function whOptionsFromStock(rows, fieldName, selectedId = 0){
  const list = rows || [];
  if (!list.length) return '<option value="0">Sin almacén configurado para este producto/localidad</option>';
  return list.map(w => `<option value="${w.warehouse_id}" ${Number(w.warehouse_id)===Number(selectedId)?'selected':''}>${esc(w.code)} · disponible ${fmt(w.qty || 0)}</option>`).join('');
}

async function openLocReqDetail(id){
  const j = await apiGet('locreq_detail', {id});
  if (!j.ok) { alert(j.error || 'Error al cargar solicitud'); return; }
  const r = j.request;
  const items = j.items || [];
  $('locReqDetailTitle').textContent = `Solicitud SL-${String(r.id).padStart(6,'0')} · ${String(r.status).toUpperCase()}`;
  $('locReqDetailSub').textContent = `${r.request_location_name} solicita a ${r.source_location_name}`;

  let html = `
    <div class="row g-3 mb-3">
      <div class="col-12 col-md-4"><div class="item-row"><div class="muted small">Solicita</div><div class="fw-bold">${esc(r.request_location_name)}</div><div class="small muted">Usuario: ${esc(r.requested_by_name || '')}</div></div></div>
      <div class="col-12 col-md-4"><div class="item-row"><div class="muted small">Proveedor</div><div class="fw-bold">${esc(r.source_location_name)}</div></div></div>
      <div class="col-12 col-md-4"><div class="item-row"><div class="muted small">Autorización / rechazo</div><div class="fw-bold">${String(r.status || '').toUpperCase()==='RECHAZADA' ? esc(r.rejected_by_name || 'Rechazo registrado') : esc(r.approved_by_name || r.shipped_by_name || 'Pendiente')}</div><div class="small muted">${r.approved_at ? 'Autorizado: '+esc(r.approved_at) : (r.rejected_at ? 'Rechazado: '+esc(r.rejected_at) : '')}</div></div></div>
    </div>
    ${String(r.status || '').toUpperCase()==='RECHAZADA' ? `<div class="alert alert-danger py-2"><strong>Motivo del rechazo:</strong> ${esc(r.reject_reason || 'Sin motivo registrado')}</div>` : ''}
    <div class="table-responsive"><table class="table table-sm align-middle">
      <thead><tr><th>Producto</th><th class="text-end">Solicitado</th><th class="text-end">Enviado</th><th class="text-end">Recibido</th><th>Almacén origen</th><th>Almacén destino</th></tr></thead><tbody>`;
  for (const it of items) {
    html += `<tr>
      <td><div class="fw-semibold">${esc(it.sku)}</div><div class="small muted">${esc(it.product_name || '')}</div></td>
      <td class="text-end mono">${fmt(it.requested_qty)}</td>
      <td class="text-end mono">${it.shipped_qty == null ? '-' : fmt(it.shipped_qty)}</td>
      <td class="text-end mono">${it.received_qty == null ? '-' : fmt(it.received_qty)}</td>
      <td>${esc(it.source_warehouse_code || '-')}</td>
      <td>${esc(it.destination_warehouse_code || '-')}</td>
    </tr>`;
  }
  html += `</tbody></table></div>`;

  const status = String(r.status || '').toUpperCase();
  if (status === 'PENDIENTE' || status === 'APROBADA') {
    html += `<hr><form id="locReqShipForm"><div class="section-title mb-2"><i class="bi bi-truck text-warning"></i> Confirmar envío</div>
      <div class="row g-3 mb-3">
        <div class="col-12 col-md-4"><label class="form-label">Fecha envío</label><input class="form-control" type="date" name="work_date" value="${operationalDateForForms()}"></div>
        <div class="col-12 col-md-4"><label class="form-label">Chofer</label><input class="form-control" name="driver_name" placeholder="Nombre"></div>
        <div class="col-12 col-md-4"><label class="form-label">Unidad</label><input class="form-control" name="vehicle_unit" placeholder="Unidad / placas"></div>
      </div>`;
    for (const it of items) {
      html += `<div class="item-row mb-2" data-ship-item="${it.id}">
        <div class="row g-3 align-items-end">
          <div class="col-12 col-md-5"><div class="fw-semibold">${esc(it.sku)}</div><div class="small muted">${esc(it.product_name || '')}</div></div>
          <div class="col-12 col-md-4"><label class="form-label">Almacén origen</label><select class="form-select ship-wh">${whOptionsFromStock(it.source_stock || [], 'source')}</select></div>
          <div class="col-12 col-md-3"><label class="form-label">Cantidad a enviar</label><input class="form-control mono ship-qty" value="${fmt(it.requested_qty)}"></div>
        </div>
      </div>`;
    }
    html += `</form>`;
  }
  if (status === 'ENVIADA') {
    html += `<hr><form id="locReqReceiveForm"><div class="section-title mb-2"><i class="bi bi-box-arrow-in-down text-warning"></i> Confirmar recepción</div>
      <div class="row g-3 mb-3"><div class="col-12 col-md-4"><label class="form-label">Fecha recepción</label><input class="form-control" type="date" name="work_date" value="${operationalDateForForms()}"></div></div>`;
    for (const it of items) {
      html += `<div class="item-row mb-2" data-recv-item="${it.id}">
        <div class="row g-3 align-items-end">
          <div class="col-12 col-md-6"><div class="fw-semibold">${esc(it.sku)}</div><div class="small muted">Enviado: ${fmt(it.shipped_qty || 0)} ${esc(it.uom || '')}</div></div>
          <div class="col-12 col-md-6"><label class="form-label">Almacén destino</label><select class="form-select recv-wh">${whOptionsFromStock(it.dest_allowed_warehouses || [], 'dest')}</select></div>
        </div>
      </div>`;
    }
    html += `</form>`;
  }
  $('locReqDetailBody').innerHTML = html;

  let footer = `<button type="button" class="btn toolbar-btn" data-bs-dismiss="modal">Cerrar</button>`;
  if (status === 'PENDIENTE' || status === 'APROBADA') {
    footer += `<button type="button" class="btn btn-outline-danger" onclick="rejectLocReq(${r.id})"><i class="bi bi-x-circle me-1"></i>Rechazar</button>`;
    footer += `<button type="button" class="btn btn-blue-soft" onclick="shipLocReq(${r.id})"><i class="bi bi-truck me-1"></i>Confirmar envío</button>`;
  } else if (status === 'ENVIADA') {
    footer += `<button type="button" class="btn btn-green-soft" onclick="receiveLocReq(${r.id})"><i class="bi bi-check2-circle me-1"></i>Confirmar recepción</button>`;
  }
  $('locReqDetailFooter').innerHTML = footer;
  locReqDetailModal.show();
}

function rejectLocReq(id){
  $('locReqRejectId').value = String(id || 0);
  $('locReqRejectReason').value = '';
  locReqRejectModal.show();
}

async function submitLocReqReject(){
  const id = parseInt($('locReqRejectId').value || '0', 10);
  const reason = String($('locReqRejectReason').value || '').trim();
  if (!reason) {
    $('locReqRejectReason').focus();
    alert('Debes indicar el motivo del rechazo.');
    return;
  }
  const j = await apiPost('locreq_reject', {id, reason});
  if (!j.ok) { alert(j.error || 'No se pudo rechazar'); return; }
  locReqRejectModal.hide();
  locReqDetailModal.hide();
  await loadLocRequests(locReqMode);
}

async function shipLocReq(id){
  const f = document.getElementById('locReqShipForm');
  if (!f) return;
  const items = [...document.querySelectorAll('[data-ship-item]')].map(row => ({
    item_id: parseInt(row.dataset.shipItem || '0', 10),
    source_warehouse_id: parseInt(row.querySelector('.ship-wh')?.value || '0', 10),
    qty: num(row.querySelector('.ship-qty')?.value || 0)
  }));
  const invalidShip = items.some(it => !it.source_warehouse_id || it.qty <= 0);
  if (invalidShip) { alert('Selecciona almacén origen y cantidad válida para todos los productos.'); return; }
  const total = items.reduce((acc, it) => acc + num(it.qty), 0);
  openLocReqConfirmModal({
    title: `Confirmar envío SL-${String(id).padStart(6,'0')}`,
    sub: 'Este movimiento descontará inventario real de la localidad proveedora.',
    body: `<div class="alert alert-warning py-2 mb-3"><strong>Atención:</strong> se generará un OUT real en inventory_moves.</div>
           <div class="confirm-box"><div><strong>Fecha:</strong> ${esc(f.work_date.value || '')}</div><div><strong>Chofer:</strong> ${esc(f.driver_name.value || '-')}</div><div><strong>Unidad:</strong> ${esc(f.vehicle_unit.value || '-')}</div><div><strong>Total piezas:</strong> ${fmt(total)}</div></div>`,
    okText: '<i class="bi bi-truck me-1"></i>Confirmar envío',
    okClass: 'btn btn-blue-soft',
    onConfirm: async () => {
      const j = await apiPost('locreq_ship', {id, work_date:f.work_date.value, driver_name:f.driver_name.value, vehicle_unit:f.vehicle_unit.value, items});
      if (!j.ok) { alert(j.error || 'No se pudo confirmar envío'); return; }
      locReqConfirmModal.hide();
      locReqDetailModal.hide();
      await reloadAll();
      await loadLocRequests('transit');
    }
  });
}

async function receiveLocReq(id){
  const f = document.getElementById('locReqReceiveForm');
  if (!f) return;
  const items = [...document.querySelectorAll('[data-recv-item]')].map(row => ({
    item_id: parseInt(row.dataset.recvItem || '0', 10),
    destination_warehouse_id: parseInt(row.querySelector('.recv-wh')?.value || '0', 10)
  }));
  const invalidRecv = items.some(it => !it.destination_warehouse_id);
  if (invalidRecv) { alert('Selecciona almacén destino para todos los productos. Si no aparece, configura el almacén del producto en Admin.'); return; }
  openLocReqConfirmModal({
    title: `Confirmar recepción SL-${String(id).padStart(6,'0')}`,
    sub: 'Este movimiento aumentará inventario real en la localidad solicitante.',
    body: `<div class="alert alert-success py-2 mb-3"><strong>Confirmación:</strong> se generará un IN real en inventory_moves.</div>
           <div class="confirm-box"><strong>Fecha recepción:</strong> ${esc(f.work_date.value || '')}<br><span class="muted small">Valida que el almacén destino sea correcto antes de continuar.</span></div>`,
    okText: '<i class="bi bi-check2-circle me-1"></i>Confirmar recepción',
    okClass: 'btn btn-green-soft',
    onConfirm: async () => {
      const j = await apiPost('locreq_receive', {id, work_date:f.work_date.value, items});
      if (!j.ok) { alert(j.error || 'No se pudo confirmar recepción'); return; }
      locReqConfirmModal.hide();
      locReqDetailModal.hide();
      await reloadAll();
      await loadLocRequests('history');
    }
  });
}

function showModuleSection(sectionId){
  document.querySelectorAll('.module-section').forEach(x => x.classList.remove('active'));
  document.querySelectorAll('.operation-nav-btn').forEach(x => x.classList.remove('active'));
  const section = document.getElementById(sectionId);
  if (section) section.classList.add('active');
  const nav = document.querySelector(`.operation-nav-btn[data-ops-section="${sectionId}"]`);
  if (nav) nav.classList.add('active');
}

function renderSalesPager(){
  const box = $('salesPager');
  if (!box) return;

  if (salesPages <= 1) {
    box.innerHTML = salesTotal > 0
      ? `<div class="small muted">Mostrando ${salesRows.length} de ${salesTotal} pedidos.</div>`
      : '';
    return;
  }

  let html = `
    <div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
      <div class="small muted">
        Página ${salesPage} de ${salesPages} · Total pedidos: ${salesTotal}
      </div>
      <div class="d-flex gap-2 flex-wrap">
  `;

  html += `
    <button class="btn toolbar-btn btn-sm" type="button"
      ${salesPage <= 1 ? 'disabled' : ''}
      onclick="loadSalesOrders(${salesPage - 1})">
      Anterior
    </button>
  `;

  let dotsLeft = false;
  let dotsRight = false;
  for (let p = 1; p <= salesPages; p++) {
    const show = p === 1 || p === salesPages || Math.abs(p - salesPage) <= 1;
    if (show) {
      html += `
        <button class="btn btn-sm ${p === salesPage ? 'btn-blue-soft' : 'toolbar-btn'}"
          type="button"
          onclick="loadSalesOrders(${p})">
          ${p}
        </button>
      `;
    } else if (p < salesPage && !dotsLeft) {
      html += `<span class="px-1 align-self-center">...</span>`;
      dotsLeft = true;
    } else if (p > salesPage && !dotsRight) {
      html += `<span class="px-1 align-self-center">...</span>`;
      dotsRight = true;
    }
  }

  html += `
    <button class="btn toolbar-btn btn-sm" type="button"
      ${salesPage >= salesPages ? 'disabled' : ''}
      onclick="loadSalesOrders(${salesPage + 1})">
      Siguiente
    </button>
  `;

  html += `</div></div>`;
  box.innerHTML = html;
}

async function loadCatalogs(){
  const [p, c, w, m] = await Promise.all([
    apiGet('products'),
    apiGet('channels'),
    apiGet('warehouses'),
    apiGet('merma_concepts')
  ]);

  products = p.ok ? (p.rows || []) : [];
  channels = c.ok ? (c.rows || []) : [];
  warehouses = w.ok ? (w.rows || []) : [];
  mermaConcepts = m.ok ? (m.rows || []) : [];

  const bar = IS_HIELO_BARRA_SCOPE ? products.find(x => String(x.sku).toUpperCase() === 'BARRA 150 KILOS') : null;
  barProductId = bar ? parseInt(bar.id, 10) : 0;
}

async function loadSalesOrders(page = 1){
  if (!IS_HIELO_BARRA_SCOPE) {
    salesRows = [];
    salesTotal = 0;
    if ($('countVH')) $('countVH').textContent = '0';
    return;
  }

  const j = await apiGet('vh_authorized', {
    page,
    pageSize: salesPageSize
  });

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

  salesRows = j.rows || [];
  salesPage = parseInt(j.page || 1, 10);
  salesPageSize = parseInt(j.pageSize || 10, 10);
  salesPages = parseInt(j.pages || 1, 10);
  salesTotal = parseInt(j.total || 0, 10);

  renderSalesOrders();
  renderSalesPager();
}

async function loadInventoryForSelectedDates(){
  initHistoryDates();
  const dateFrom = $('histDateFrom')?.value || getMonthRange().from;
  const dateTo = $('histDateTo')?.value || getMonthRange().to;
  const inv = await apiGet('inventory_total', {date_from: dateFrom, date_to: dateTo});
  if (!inv.ok) {
    alert(inv.error || 'Error al cargar inventario');
    return false;
  }

  invRows = inv.rows || [];
  barProductId = inv.bar_product_id || barProductId;
  renderInventory();
  return true;
}

async function reloadAll(){
  const invOk = await loadInventoryForSelectedDates();
  if (!invOk) return;

  const mv = await apiGet('moves_list_total');
  if (!mv.ok) {
    alert(mv.error || 'Error al cargar movimientos');
    return;
  }
  const moveRows = mv.rows || [];
  renderMoves(moveRows);
  renderReturns(moveRows);
  await loadMovementSummary();
  renderProductCards();

  await loadSalesOrders(salesPage);
  await loadSalesCreatedOrders();
  await loadWarehouseMermas();
  await loadLocRequests(locReqMode);
  updateModuleCounts(moveRows, mermaRows || []);
  await loadDispatchMailbox();
}

function channelOptions(){
  return channels.map(c => `<option value="${c.id}">${esc(c.name)} (${esc(c.channel_type)})</option>`).join('');
}
function productOptions(){
  return products.map(p => `<option value="${p.id}">${esc(p.sku)} · ${esc(p.name)}</option>`).join('');
}
function warehouseOptions(){
  return warehouses.map(w => `<option value="${w.id}">${esc(w.code)} · ${esc(w.name)}</option>`).join('');
}


function mermaConceptOptions(){
  return mermaConcepts.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('');
}

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

  if (!rows.length) {
    $('mermaEmptyHint').textContent = 'Sin mermas de almacén registradas.';
    tb.innerHTML = `<tr><td colspan="6" class="text-center muted py-4">Sin mermas registradas.</td></tr>`;
    return;
  }

  $('mermaEmptyHint').textContent = '';
  for (const r of rows) {
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td><div class="mono fw-semibold">${esc(r.work_date || '')}</div><div class="small muted mono">${esc(String(r.created_at || '').substring(11,19))}</div></td>
        <td>
          <div class="fw-semibold" >${esc(r.sku || '')}</div>
          <div class="small" >${esc(r.product_name || '')}</div>
        </td>
        <td>
          <div class="fw-semibold">${esc(r.warehouse_code || '')}</div>
          <div class="small" >${esc(r.warehouse_name || '')}</div>
        </td>
        <td>${esc(r.concept_name || '')}</td>
        <td class="text-end mono fw-bold">${fmt(r.qty)}</td>
        <td class="small" >${esc(r.notes || '')}</td>
      </tr>
    `);
  }
}

async function loadWarehouseMermas(page=mermaPage){
  initHistoryDates();
  mermaPage = page || 1;
  const dateFrom = $('histDateFrom')?.value || getMonthRange().from;
  const dateTo = $('histDateTo')?.value || getMonthRange().to;
  const j = await apiGet('warehouse_merma_list', {date_from: dateFrom, date_to: dateTo, page: mermaPage, per_page: mermaPageSize});
  if (!j.ok) {
    alert(j.error || 'Error al cargar mermas de almacén');
    return;
  }
  mermaRows = j.rows || [];
  mermaPagination = j.pagination || {};
  renderWarehouseMermas(mermaRows);
  renderMermaPager(mermaPagination);
}

function renderMermaPager(pagination){
  const pg = $('mermaPager');
  if (!pg) return;
  const page = Number(pagination.page || 1);
  const totalPages = Number(pagination.total_pages || 1);
  const totalRows = Number(pagination.total_rows || 0);
  pg.innerHTML = `
    <div class="small muted">Página ${page} de ${totalPages} · Registros: ${totalRows}</div>
    <div class="d-flex gap-2">
      <button class="btn toolbar-btn btn-sm" type="button" id="btnMermaPrev" ${page <= 1 ? 'disabled' : ''}>Anterior</button>
      <button class="btn toolbar-btn btn-sm" type="button" id="btnMermaNext" ${page >= totalPages ? 'disabled' : ''}>Siguiente</button>
    </div>
  `;
  if ($('btnMermaPrev')) $('btnMermaPrev').addEventListener('click', () => loadWarehouseMermas(page - 1));
  if ($('btnMermaNext')) $('btnMermaNext').addEventListener('click', () => loadWarehouseMermas(page + 1));
}

async function refreshWarehouseStockHint(){
  const productId = parseInt($('mProduct')?.value || '0', 10);
  const warehouseId = parseInt($('mWarehouse')?.value || '0', 10);
  const hint = $('mWarehouseStockHint');
  if (!hint) return;
  if (productId <= 0 || warehouseId <= 0) {
    hint.textContent = 'Disponible en almacén: 0';
    return;
  }
  const j = await apiGet('warehouse_stock', {product_id: productId, warehouse_id: warehouseId});
  if (!j.ok) {
    hint.textContent = 'Disponible en almacén: N/D';
    return;
  }
  hint.textContent = `Disponible en almacén: ${fmt(j.available || 0)}`;
}

function openMermaModal(){
  $('mermaForm').reset();
  $('mProduct').innerHTML = productOptions();
  $('mWarehouse').innerHTML = warehouseOptions();
  $('mConcept').innerHTML = mermaConceptOptions();

  $('mermaForm').querySelector('[name="work_date"]').value = operationalDateForForms();

  refreshWarehouseStockHint();
  mermaModal.show();
}

function collectMermaForm(){
  const fd = new FormData($('mermaForm'));
  const payload = Object.fromEntries(fd.entries());
  payload.product_id = parseInt(payload.product_id || '0', 10);
  payload.warehouse_id = parseInt(payload.warehouse_id || '0', 10);
  payload.concept_id = parseInt(payload.concept_id || '0', 10);
  payload.qty = parseFloat(String(payload.qty || '0').replace(',', '.')) || 0;
  return payload;
}

async function saveWarehouseMerma(){
  const payload = collectMermaForm();
  const j = await apiPost('warehouse_merma_save', payload);
  if (!j.ok) {
    alert(j.error || 'No se pudo guardar la merma');
    return;
  }
  mermaModal.hide();
  await reloadAll();
  alert('Merma registrada correctamente.');
}


function renderReturns(moveRows = []){
  const tb = $('returnBody');
  if (!tb) return;
  const rows = (moveRows || []).filter(r => String(r.ref_type || '').toUpperCase() === 'WAREHOUSE_RETURN');
  tb.innerHTML = '';
  if (!rows.length) {
    if ($('returnEmptyHint')) $('returnEmptyHint').textContent = 'Sin devoluciones registradas.';
    tb.innerHTML = `<tr><td colspan="7" class="text-center muted py-4">Sin devoluciones registradas.</td></tr>`;
    return;
  }
  if ($('returnEmptyHint')) $('returnEmptyHint').textContent = '';
  for (const r of rows) {
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td><div class="mono fw-semibold">${esc(r.work_date || '')}</div><div class="small muted mono">${esc(String(r.created_at || '').substring(11,19))}</div></td>
        <td>
          <div class="fw-semibold">${esc(r.sku || '')}</div>
          <div class="small">${esc(r.product_name || '')}</div>
        </td>
        <td>
          <div class="fw-semibold">${esc(r.warehouse_code || '')}</div>
          <div class="small">${esc(r.warehouse_name || '')}</div>
        </td>
        <td><span class="badge text-bg-success">IN devolución</span></td>
        <td class="small">${esc(r.notes || '')}</td>
        <td class="small">${esc(r.user_name || r.username || '')}</td>
        <td class="text-end mono fw-bold">${fmt(r.qty)}</td>
      </tr>
    `);
  }
}

async function loadReturnWarehouses(){
  const productId = parseInt($('rProduct')?.value || '0', 10);
  const sel = $('rWarehouse');
  if (!sel) return;
  sel.innerHTML = '';
  if (productId <= 0) {
    sel.innerHTML = `<option value="">Selecciona producto</option>`;
    return;
  }
  const j = await apiGet('return_warehouses', {product_id: productId});
  if (!j.ok) {
    sel.innerHTML = `<option value="">Error al cargar almacenes</option>`;
    if ($('rWarehouseStockHint')) $('rWarehouseStockHint').textContent = j.error || 'No se pudieron cargar almacenes.';
    return;
  }
  const rows = j.rows || [];
  if (!rows.length) {
    sel.innerHTML = `<option value="">Sin almacén configurado</option>`;
    if ($('rWarehouseStockHint')) $('rWarehouseStockHint').textContent = 'Configura almacén de entrada en Admin > Productos.';
    return;
  }
  sel.innerHTML = rows.map(w => `<option value="${w.warehouse_id}">${esc(w.code)} · ${esc(w.name || '')} · disponible ${fmt(w.qty || 0)}</option>`).join('');
  await refreshReturnWarehouseStockHint();
}

async function refreshReturnWarehouseStockHint(){
  const productId = parseInt($('rProduct')?.value || '0', 10);
  const warehouseId = parseInt($('rWarehouse')?.value || '0', 10);
  const hint = $('rWarehouseStockHint');
  if (!hint) return;
  if (productId <= 0 || warehouseId <= 0) {
    hint.textContent = 'Disponible actual: 0';
    return;
  }
  const j = await apiGet('warehouse_stock', {product_id: productId, warehouse_id: warehouseId});
  if (!j.ok) {
    hint.textContent = 'Disponible actual: N/D';
    return;
  }
  hint.textContent = `Disponible actual: ${fmt(j.available || 0)}`;
}

async function openReturnModal(){
  $('returnForm').reset();
  $('rProduct').innerHTML = productOptions();

  $('returnForm').querySelector('[name="work_date"]').value = operationalDateForForms();

  await loadReturnWarehouses();
  returnModal.show();
}

function collectReturnForm(){
  const fd = new FormData($('returnForm'));
  const payload = Object.fromEntries(fd.entries());
  payload.product_id = parseInt(payload.product_id || '0', 10);
  payload.warehouse_id = parseInt(payload.warehouse_id || '0', 10);
  payload.qty = parseFloat(String(payload.qty || '0').replace(',', '.')) || 0;
  return payload;
}

function buildWarehouseReturnStockSummary(data, confirmed = false){
  return `
    <div class="mb-3">
      <div class="small muted">${confirmed ? 'Movimiento aplicado' : 'Resumen del movimiento a registrar'}</div>
      <div class="fw-bold">${esc(data.product_sku || '')} · ${esc(data.product_name || '')}</div>
      <div class="small muted">Destino: <span class="mono fw-semibold">${esc(data.warehouse_code || '')}</span> · ${esc(data.warehouse_name || '')}</div>
    </div>
    <div class="table-wrap mb-3">
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead>
            <tr>
              <th>Almacén destino</th>
              <th class="text-end">Antes</th>
              <th class="text-end">Entrada devolución</th>
              <th class="text-end">Después</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td><div class="mono fw-bold">${esc(data.warehouse_code || '')}</div><div class="small muted">${esc(data.warehouse_name || '')}</div></td>
              <td class="text-end mono fw-bold">${fmt(data.stock_before || 0)}</td>
              <td class="text-end mono fw-bold text-success">+${fmt(data.qty || 0)}</td>
              <td class="text-end mono fw-bold text-primary">${fmt(data.stock_after || 0)}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    <div class="row g-2 small">
      <div class="col-12 col-md-6"><div class="item-row"><div class="muted">Referencia salida</div><div class="mono fw-semibold">${esc(data.reference || '')}</div></div></div>
      <div class="col-12 col-md-6"><div class="item-row"><div class="muted">Motivo</div><div class="fw-semibold">${esc(data.reason || '')}</div></div></div>
    </div>
  `;
}

async function saveWarehouseReturn(){
  const payload = collectReturnForm();
  if (!payload.reference || !String(payload.reference).trim()) {
    alert('Indica la referencia o número de salida original.');
    return;
  }
  if (!payload.product_id || !payload.warehouse_id || payload.qty <= 0) {
    alert('Producto, almacén destino y cantidad son obligatorios.');
    return;
  }
  if (!payload.reason || !String(payload.reason).trim()) {
    alert('Indica el motivo de la devolución.');
    return;
  }

  // Solo consulta el impacto; todavía no se registra movimiento.
  const preview = await apiPost('warehouse_return_preview', payload);
  if (!preview.ok) {
    alert(preview.error || 'No se pudo validar la devolución');
    return;
  }

  pendingWarehouseReturnPayload = payload;
  $('returnConfirmSummary').innerHTML = buildWarehouseReturnStockSummary(preview, false);
  returnModal.hide();
  returnConfirmModal.show();
}

async function confirmWarehouseReturn(){
  if (!pendingWarehouseReturnPayload) return;

  const btn = $('btnConfirmWarehouseReturn');
  const originalHtml = btn.innerHTML;
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Registrando...';

  try {
    const j = await apiPost('warehouse_return_save', pendingWarehouseReturnPayload);
    if (!j.ok) {
      alert(j.error || 'No se pudo guardar la devolución');
      return;
    }

    pendingWarehouseReturnPayload = null;
    returnConfirmModal.hide();
    $('returnResultSummary').innerHTML = buildWarehouseReturnStockSummary(j, true);
    await reloadAll();
    returnResultModal.show();
  } finally {
    btn.disabled = false;
    btn.innerHTML = originalHtml;
  }
}

function createItemRow(item = {}){
  const div = document.createElement('div');
  div.className = 'item-row';
  div.innerHTML = `
    <div class="row g-3 align-items-end">
      <div class="col-12 col-lg-8">
        <label class="form-label">Producto</label>
        <select class="form-select" name="item_product_id">${productOptions()}</select>
      </div>
      <div class="col-8 col-lg-3">
        <label class="form-label">Cantidad</label>
        <input class="form-control mono" name="item_qty" inputmode="decimal" value="${esc(item.qty || '0')}">
      </div>
      <div class="col-4 col-lg-1">
        <button type="button" class="btn toolbar-btn w-100 btnRemoveItem">
          <i class="bi bi-x-lg"></i>
        </button>
      </div>
    </div>
  `;

  const sel = div.querySelector('[name="item_product_id"]');
  if (sel) sel.value = String(item.product_id || products[0]?.id || '');

  const removeBtn = div.querySelector('.btnRemoveItem');
  if (removeBtn) removeBtn.addEventListener('click', () => div.remove());

  return div;
}

function openModal(){
  $('editForm').reset();
  $('itemsWrap').innerHTML = '';
  $('fChannel').innerHTML = channelOptions();

  $('editForm').querySelector('[name="work_date"]').value = operationalDateForForms();

  $('itemsWrap').appendChild(createItemRow());
  modal.show();
}

function openTransferModal(){
  $('transferForm').reset();
  $('tProduct').innerHTML = productOptions();
  $('tFromWarehouse').innerHTML = warehouseOptions();
  $('tToWarehouse').innerHTML = warehouseOptions();

  $('transferForm').querySelector('[name="work_date"]').value = operationalDateForForms();

  transferModal.show();
}

function collectForm(){
  const fd = new FormData($('editForm'));
  const payload = Object.fromEntries(fd.entries());

  payload.channel_id = parseInt(payload.channel_id || '0', 10);
  payload.items = [];

  document.querySelectorAll('#itemsWrap .item-row').forEach(row => {
    const productId = parseInt(row.querySelector('[name="item_product_id"]').value || '0', 10);
    const qty = parseFloat(String(row.querySelector('[name="item_qty"]').value || '0').replace(',', '.')) || 0;
    if (productId > 0 && qty > 0) {
      payload.items.push({product_id: productId, qty});
    }
  });

  return payload;
}

function collectTransferForm(){
  const fd = new FormData($('transferForm'));
  const payload = Object.fromEntries(fd.entries());

  payload.product_id = parseInt(payload.product_id || '0', 10);
  payload.from_warehouse_id = parseInt(payload.from_warehouse_id || '0', 10);
  payload.to_warehouse_id = parseInt(payload.to_warehouse_id || '0', 10);
  payload.qty = parseFloat(String(payload.qty || '0').replace(',', '.')) || 0;

  return payload;
}

async function saveOut(){
  const payload = collectForm();
  const j = await apiPost('out_save', payload);
  if (!j.ok) {
    alert(j.error || 'No se pudo guardar');
    return;
  }
  modal.hide();
  await reloadAll();
}

async function saveTransfer(){
  const payload = collectTransferForm();
  const j = await apiPost('transfer_save', payload);
  if (!j.ok) {
    alert(j.error || 'No se pudo guardar la transferencia');
    return;
  }
  transferModal.hide();
  await reloadAll();
  alert('Transferencia guardada correctamente.');
}

async function delMove(id){
  if (!confirm('¿Deseas eliminar este movimiento?')) return;
  const j = await apiPost('delete', {id});
  if (!j.ok) {
    alert(j.error || 'No se pudo eliminar');
    return;
  }
  await reloadAll();
}

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

  $('barsUsedKpi').textContent = fmtBar(j.bars_used || 0);
  $('detailNote').innerHTML = `<i class="bi bi-info-circle me-2"></i>${esc(j.note || '')}`;

  const bw = $('barsWhBody');
  bw.innerHTML = '';
  for (const r of (j.bars_by_wh || [])) {
    bw.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="fw-semibold" >${esc(r.code)} · ${esc(r.name)}</td>
        <td class="text-end mono fw-bold">${fmtBar(r.qty)}</td>
      </tr>
    `);
  }
  if (!(j.bars_by_wh || []).length) {
    bw.innerHTML = `<tr><td colspan="2" class="text-center muted py-3">Sin barra comercial en CF1/CF2.</td></tr>`;
  }

  const bi = $('barsInternalBody');
  if (bi) {
    bi.innerHTML = '';
    for (const r of (j.bars_internal_wh || [])) {
      bi.insertAdjacentHTML('beforeend', `
        <tr>
          <td>
            <div class="fw-semibold">${esc(r.code)} · ${esc(r.name)}</div>
            <div class="small muted">Inventario físico interno; no suma al disponible comercial.</div>
          </td>
          <td class="text-end mono fw-bold ${num(r.qty) < 0 ? 'text-danger' : ''}">${fmtBar(r.qty)}</td>
        </tr>
      `);
    }
    if (!(j.bars_internal_wh || []).length) {
      bi.innerHTML = `<tr><td colspan="2" class="text-center muted py-3">Sin barra en almacenes internos.</td></tr>`;
    }
  }

  const cf = $('ptBody');
  cf.innerHTML = '';
  for (const r of (j.pt_products || [])) {
    cf.insertAdjacentHTML('beforeend', `
      <tr>
        <td>
          <div class="fw-semibold" >${esc(r.sku)}</div>
          <div class="small" >${esc(r.name)}</div>
        </td>
        <td class="text-end mono fw-bold">${fmt(r.qty)}</td>
        <td class="mono">${esc(r.uom || '')}</td>
      </tr>
    `);
  }
  if (!(j.pt_products || []).length) {
    cf.innerHTML = `<tr><td colspan="3" class="text-center muted py-3">Sin productos.</td></tr>`;
  }

  detailModal.show();
}

async function openSalesOrderDetail(orderId){
  const j = await apiGet('vh_order_detail', {order_id: orderId});
  if (!j.ok) {
    alert(j.error || 'No se pudo cargar el detalle del pedido de VentasHielo.');
    return;
  }

  currentSalesOrder = Object.assign({}, j.order || {}, {source_mode:'VH'});
  const order = j.order || {};
  const items = j.items || [];
  const folio = order.sales_order || order.ov_system || ('PW-' + order.id);

  $('sdOrderId').value = String(order.id || '');
  $('sdChannelId').innerHTML = channelOptions();
  $('sdWorkDate').value = operationalDateForForms();
  $('sdReference').value = folio;
  $('sdCustomer').value = order.cliente || '';
  $('sdDriverName').value = '';
  $('sdVehicleUnit').value = '';
  $('sdDepartAt').value = '';
  $('sdNotes').value = order.comments || '';

  $('salesDetailHeader').innerHTML = `
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Folio</div>
          <div class="fw-bold mono">${esc(folio)}</div>
        </div>
      </div>
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Cliente</div>
          <div class="fw-bold">${esc(order.cliente || '')}</div>
          <div class="small">${esc(order.cliente_phone || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Fecha solicitada</div>
          <div class="fw-semibold">${esc(order.delivery_date || '')} ${esc(order.delivery_time || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="item-row">
          <div class="small">Dirección entrega</div>
          <div class="fw-semibold">${esc(order.delivery_address || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="item-row">
          <div class="small">Lugar / etiqueta</div>
          <div class="fw-semibold">${esc(order.address_label || '—')}</div>
        </div>
      </div>
      <div class="col-12">
        <div class="item-row">
          <div class="small">Instrucciones de entrega</div>
          <div class="fw-semibold">${esc(order.delivery_instructions || 'Sin instrucciones de entrega')}</div>
        </div>
      </div>
    `;

  $('salesDetailItems').innerHTML = items.map(it => `
    <tr>
      <td class="mono">${esc(it.breport_product_id || '')}</td>
      <td>
        <div class="fw-semibold">${esc(it.product_name || '')}</div>
        <div class="small">VH #${esc(it.product_id || '')}</div>
      </td>
      <td class="text-end mono fw-bold">${fmt(it.quantity)}</td>
      <td class="mono">PZA</td>
    </tr>
  `).join('');

  $('btnDispatchSalesOrder').classList.toggle('d-none', !CAN_WRITE);
  salesDetailModal.show();
}



async function openBreportSalesOrderDetail(orderId){
  const j = await apiGet('sales_order_detail', {order_id: orderId});
  if (!j.ok) {
    alert(j.error || 'No se pudo cargar el detalle de la orden creada desde ventas.');
    return;
  }

  currentSalesOrder = Object.assign({}, j.order || {}, {source_mode:'BREPORT'});
  const order = j.order || {};
  const items = j.items || [];
  const folio = order.sales_order_ref || ('PED-' + order.id);

  $('sdOrderId').value = String(order.id || '');
  $('sdChannelId').innerHTML = channelOptions();
  $('sdWorkDate').value = operationalDateForForms();
  $('sdReference').value = folio;
  $('sdCustomer').value = order.business_name || '';
  $('sdDriverName').value = '';
  $('sdVehicleUnit').value = '';
  $('sdDepartAt').value = '';
  $('sdNotes').value = order.comments || '';

  $('salesDetailHeader').innerHTML = `
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Folio</div>
          <div class="fw-bold mono">${esc(folio)}</div>
        </div>
      </div>
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Cliente / destino</div>
          <div class="fw-bold">${esc(order.business_name || '')}</div>
          <div class="small">${esc(order.phone || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-4">
        <div class="item-row">
          <div class="small">Fecha solicitada</div>
          <div class="fw-semibold">${esc(order.requested_delivery_date || '')} ${esc(order.requested_delivery_time || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="item-row">
          <div class="small">Dirección / referencia</div>
          <div class="fw-semibold">${esc(order.delivery_address || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-3">
        <div class="item-row">
          <div class="small">Ruta</div>
          <div class="fw-semibold mono">${esc(order.route_no ? ('Ruta ' + order.route_no) : 'Sin ruta')}</div>
        </div>
      </div>
      <div class="col-12 col-md-3">
        <div class="item-row">
          <div class="small">Estatus</div>
          <div class="fw-semibold">${esc(order.status || '')}</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="item-row">
          <div class="small">Origen</div>
          <div class="fw-semibold">Orden creada desde Ventas</div>
        </div>
      </div>
    `;

  $('salesDetailItems').innerHTML = items.map(it => `
    <tr>
      <td class="mono">${esc(it.sku || '')}</td>
      <td>
        <div class="fw-semibold">${esc(it.product_name || '')}</div>
        <div class="small">Producto BReport #${esc(it.product_id || '')}</div>
      </td>
      <td class="text-end mono fw-bold">${fmt(it.qty)}</td>
      <td class="mono">${esc(it.uom || '')}</td>
      <td class="text-end mono fw-bold">${fmt(it.qty)}</td>
    </tr>
  `).join('');

  const canDispatchOrder = CAN_WRITE && String(order.status || '').toUpperCase() === 'AUTORIZADA' && !order.outbound_id;
  $('btnDispatchSalesOrder').classList.toggle('d-none', !canDispatchOrder);

  salesDetailModal.show();
}

function buildPendingHtml(items){
  if (!Array.isArray(items) || !items.length) {
    return '<div class="text-success fw-semibold"><i class="bi bi-check2-circle me-1"></i>Todos los artículos relacionados disponibles fueron procesados.</div>';
  }

  const rows = items.map(it => {
    const sku = it.sku ? `<div class="small mono text-muted">${esc(it.sku)}</div>` : '';
    const name = esc(it.product_name || ('Producto ' + (it.product_id || it.vh_product_id || '')));
    const req = fmt(it.requested_qty || 0);
    const disp = fmt(it.available_qty || 0);
    const surt = fmt(it.dispatched_qty || 0);
    const falt = fmt(it.missing_qty || 0);
    const reason = esc(it.reason || 'Artículo omitido o surtido parcial.');
    return `
      <tr>
        <td>${name}${sku}<div class="small text-muted">${reason}</div></td>
        <td class="text-end mono">${req}</td>
        <td class="text-end mono">${disp}</td>
        <td class="text-end mono fw-semibold">${surt}</td>
        <td class="text-end mono text-danger fw-semibold">${falt}</td>
      </tr>
    `;
  }).join('');

  return `
    <div class="alert alert-warning mb-2">
      <i class="bi bi-info-circle me-2"></i>
      Algunos artículos no se descontaron o se surtieron parcialmente.
    </div>
    <div class="table-responsive">
      <table class="table table-sm align-middle mb-0">
        <thead>
          <tr>
            <th>Artículo</th>
            <th class="text-end">Solicitado</th>
            <th class="text-end">Disponible</th>
            <th class="text-end">A surtir</th>
            <th class="text-end">Faltante</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>
    </div>
  `;
}

function buildDispatchableHtml(items){
  if (!Array.isArray(items) || !items.length) {
    return '<div class="alert alert-danger mb-0">No hay artículos con inventario disponible para generar salida.</div>';
  }
  const rows = items.map(it => `
    <tr>
      <td>
        <div class="fw-semibold">${esc(it.product_name || '')}</div>
        <div class="small mono text-muted">${esc(it.sku || '')}</div>
      </td>
      <td class="text-end mono">${fmt(it.requested_qty || 0)}</td>
      <td class="text-end mono">${fmt(it.available_qty || 0)}</td>
      <td class="text-end mono fw-bold text-success">${fmt(it.dispatched_qty || 0)}</td>
    </tr>
  `).join('');
  return `
    <div class="table-responsive">
      <table class="table table-sm align-middle mb-0">
        <thead><tr><th>Artículo</th><th class="text-end">Solicitado</th><th class="text-end">Disponible</th><th class="text-end">Se surtirá</th></tr></thead>
        <tbody>${rows}</tbody>
      </table>
    </div>
  `;
}

function buildMovementHtml(items){
  if (!Array.isArray(items) || !items.length) return '';
  const rows = items.map(it => `
    <tr>
      <td>
        <div class="fw-semibold">${esc(it.product_name || '')}</div>
        <div class="small mono text-muted">${esc(it.sku || '')}</div>
      </td>
      <td class="mono">${esc(it.warehouse_code || '')}</td>
      <td class="mono">${esc(it.move_type || 'OUT')}</td>
      <td class="text-end mono fw-bold">${fmt(it.qty || 0)}</td>
      <td class="mono">${esc(it.ref_type || '')}</td>
    </tr>
  `).join('');
  return `
    <div class="mt-3">
      <div class="fw-semibold mb-2">Movimientos generados en inventario</div>
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead><tr><th>Artículo</th><th>Almacén</th><th>Mov.</th><th class="text-end">Cantidad</th><th>Tipo</th></tr></thead>
          <tbody>${rows}</tbody>
        </table>
      </div>
    </div>
  `;
}

function showVhPartialConfirm(data){
  let el = document.getElementById('vhPartialConfirmModal');
  if (!el) {
    document.body.insertAdjacentHTML('beforeend', `
      <div class="modal fade" id="vhPartialConfirmModal" tabindex="-1">
        <div class="modal-dialog modal-dialog-centered modal-xl">
          <div class="modal-content rounded-4">
            <div class="modal-header">
              <div>
                <div class="modal-title fw-bold">Confirmar salida parcial VentasHielo</div>
                <div class="small text-muted">Solo se descontarán artículos con existencia disponible en BReport.</div>
              </div>
              <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
              <div class="alert alert-warning">
                <i class="bi bi-exclamation-triangle me-2"></i>
                El pedido tiene artículos sin inventario suficiente o sin relación con BReport. Revisa el detalle antes de continuar.
              </div>
              <div class="row g-3">
                <div class="col-12 col-xl-6">
                  <div class="fw-semibold mb-2">Artículos que sí se surtirán</div>
                  <div id="vhPartialOk"></div>
                </div>
                <div class="col-12 col-xl-6">
                  <div class="fw-semibold mb-2">Artículos sin existencia / pendientes</div>
                  <div id="vhPartialPending"></div>
                </div>
              </div>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
              <button type="button" class="btn btn-primary" id="btnVhPartialConfirmYes">Generar salida parcial</button>
            </div>
          </div>
        </div>
      </div>
    `);
    el = document.getElementById('vhPartialConfirmModal');
  }

  document.getElementById('vhPartialOk').innerHTML = buildDispatchableHtml(data.dispatchable_items || []);
  document.getElementById('vhPartialPending').innerHTML = buildPendingHtml(data.pending_items || []);

  const modalInstance = bootstrap.Modal.getOrCreateInstance(el);
  return new Promise(resolve => {
    const btn = document.getElementById('btnVhPartialConfirmYes');
    const yesHandler = () => {
      btn.removeEventListener('click', yesHandler);
      el.removeEventListener('hidden.bs.modal', noHandler);
      modalInstance.hide();
      resolve(true);
    };
    const noHandler = () => {
      btn.removeEventListener('click', yesHandler);
      el.removeEventListener('hidden.bs.modal', noHandler);
      resolve(false);
    };
    btn.addEventListener('click', yesHandler, {once:true});
    el.addEventListener('hidden.bs.modal', noHandler, {once:true});
    modalInstance.show();
  });
}

function confirmDispatch(payload){
  $('confirmDispatchSummary').innerHTML = `
    <div class="item-row mb-2">
      <div class="small">Referencia</div>
      <div class="fw-semibold mono">${esc(payload.reference || '')}</div>
    </div>
    <div class="item-row mb-2">
      <div class="small">Cliente / destino</div>
      <div class="fw-semibold">${esc(payload.customer_name || '')}</div>
    </div>
    <div class="item-row">
      <div class="small">Chofer / unidad</div>
      <div class="fw-semibold">${esc(payload.driver_name || '-')} · ${esc(payload.vehicle_unit || '-')}</div>
    </div>
  `;

  return new Promise(resolve => {
    const btn = $('btnConfirmDispatchYes');
    const handler = () => {
      btn.removeEventListener('click', handler);
      confirmDispatchModal.hide();
      resolve(true);
    };

    btn.addEventListener('click', handler, {once:true});
    const el = document.getElementById('confirmDispatchModal');
    const cancelHandler = () => {
      btn.removeEventListener('click', handler);
      el.removeEventListener('hidden.bs.modal', cancelHandler);
      resolve(false);
    };
    el.addEventListener('hidden.bs.modal', cancelHandler, {once:true});
    confirmDispatchModal.show();
  });
}


async function dispatchSalesOrder(){
  const payload = {
    order_id: parseInt($('sdOrderId').value || '0', 10),
    work_date: $('sdWorkDate').value,
    channel_id: parseInt($('sdChannelId').value || '0', 10),
    reference: $('sdReference').value.trim(),
    customer_name: $('sdCustomer').value.trim(),
    driver_name: $('sdDriverName').value.trim(),
    vehicle_unit: $('sdVehicleUnit').value.trim(),
    depart_at: $('sdDepartAt').value.trim(),
    notes: $('sdNotes').value.trim()
  };

  if (payload.order_id <= 0) {
    alert('Pedido inválido.');
    return;
  }
  if (payload.channel_id <= 0) {
    alert('Selecciona un canal.');
    return;
  }

  const confirmed = await confirmDispatch(payload);
  if (!confirmed) return;

  const btn = $('btnDispatchSalesOrder');
  const originalHtml = btn.innerHTML;
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Generando salida...';

  try {
    const dispatchAction = currentSalesOrder && currentSalesOrder.source_mode === 'BREPORT' ? 'sales_dispatch' : 'vh_dispatch';
    let j = await apiPost(dispatchAction, payload);

    if (j.ok && j.requires_confirmation && dispatchAction === 'vh_dispatch') {
      const allowPartial = await showVhPartialConfirm(j);
      if (!allowPartial) return;
      payload.confirm_partial = true;
      j = await apiPost(dispatchAction, payload);
    }

    if (!j.ok) {
      const detail = j.pending_items
        ? '\n\n' + (j.pending_items || []).map(it => `${it.product_name || it.sku || 'Artículo'}: solicitado ${fmt(it.requested_qty || 0)}, disponible ${fmt(it.available_qty || 0)}, faltante ${fmt(it.missing_qty || 0)}`).join('\n')
        : '';
      alert((j.error || 'No se pudo generar la salida.') + detail);
      return;
    }

    salesDetailModal.hide();
    await reloadAll();

    const pdfUrl = j.pdf_url || `./orden_entrega_pdf.php?order_id=${encodeURIComponent(j.sales_order_id || '')}&outbound_id=${encodeURIComponent(j.outbound_id || '')}&folio=${encodeURIComponent(j.reference || payload.reference || '')}`;

    $('btnDownloadDispatchPdf').href = pdfUrl;
    $('dispatchResultSummary').innerHTML = `
      <div class="item-row mb-2">
        <div class="small">Referencia</div>
        <div class="fw-semibold mono">${esc(j.reference || payload.reference || '')}</div>
      </div>
      <div class="item-row mb-3">
        <div class="small">Salida generada</div>
        <div class="fw-semibold">Outbound #${esc(j.outbound_id || '')}</div>
      </div>
      ${buildPendingHtml(j.pending_items || [])}
      ${buildMovementHtml(j.movement_rows || [])}
    `;
    dispatchResultModal.show();
  } finally {
    btn.disabled = false;
    btn.innerHTML = originalHtml;
  }
}


document.addEventListener('DOMContentLoaded', async () => {
  modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('editModal'));
  detailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('detailModal'));
  transferModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('transferModal'));
  returnModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('returnModal'));
  returnConfirmModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('returnConfirmModal'));
  returnResultModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('returnResultModal'));
  salesDetailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('salesDetailModal'));
  routeOrdersModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('routeOrdersModal'));
  confirmDispatchModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('confirmDispatchModal'));
  dispatchResultModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('dispatchResultModal'));
  locReqModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('locReqModal'));
  locReqDetailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('locReqDetailModal'));
  locReqConfirmModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('locReqConfirmModal'));
  locReqRejectModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('locReqRejectModal'));
  movementHistoryModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('movementHistoryModal'));
  mermaModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('mermaModal'));

  await loadSystemClock();
  setInterval(loadSystemClock, 30000);
  await loadScopeSelector();
  if ($('btnScopeChange')) $('btnScopeChange').addEventListener('click', changeScopeLocation);
  if ($('scopeLocationSel')) $('scopeLocationSel').addEventListener('change', changeScopeLocation);

  if ($('salesRouteDate')) $('salesRouteDate').value = operationalDateForForms();
  initHistoryDates();

  await loadCatalogs();
  await reloadAll();

  $('btnReload').addEventListener('click', reloadAll);
  $('btnAdd').addEventListener('click', () => { if (CAN_WRITE) openModal(); });
  if ($('btnReturn')) $('btnReturn').addEventListener('click', () => { if (CAN_WRITE) openReturnModal(); });
  if ($('btnReturnSection')) $('btnReturnSection').addEventListener('click', () => { if (CAN_WRITE) openReturnModal(); });
  $('btnMerma').addEventListener('click', () => { if (CAN_WRITE) openMermaModal(); });
  $('btnTransfer').addEventListener('click', () => { if (CAN_WRITE) openTransferModal(); });
  if ($('btnLocReqNew')) $('btnLocReqNew').addEventListener('click', () => { if (CAN_WRITE) openLocReqModal(); });
  if ($('btnLocReqReload')) $('btnLocReqReload').addEventListener('click', () => loadLocRequests(locReqMode));
  if ($('btnLocReqAddItem')) $('btnLocReqAddItem').addEventListener('click', () => $('locReqItemsWrap').appendChild(createLocReqItemRow()));
  if ($('locReqForm')) $('locReqForm').addEventListener('submit', (e) => { e.preventDefault(); saveLocReq(); });
  if ($('locReqRejectForm')) $('locReqRejectForm').addEventListener('submit', (e) => { e.preventDefault(); submitLocReqReject(); });
  $('btnAddItem').addEventListener('click', () => $('itemsWrap').appendChild(createItemRow()));
  $('btnDispatchSalesOrder').addEventListener('click', dispatchSalesOrder);

  $('editForm').addEventListener('submit', (e) => {
    e.preventDefault();
    saveOut();
  });

  $('transferForm').addEventListener('submit', (e) => {
    e.preventDefault();
    saveTransfer();
  });

  if ($('returnForm')) $('returnForm').addEventListener('submit', (e) => {
    e.preventDefault();
    saveWarehouseReturn();
  });
  if ($('btnConfirmWarehouseReturn')) $('btnConfirmWarehouseReturn').addEventListener('click', confirmWarehouseReturn);
  if ($('returnConfirmModal')) $('returnConfirmModal').addEventListener('hidden.bs.modal', () => {
    if (pendingWarehouseReturnPayload) returnModal.show();
  });
  if ($('rProduct')) $('rProduct').addEventListener('change', loadReturnWarehouses);
  if ($('rWarehouse')) $('rWarehouse').addEventListener('change', refreshReturnWarehouseStockHint);

  $('mermaForm').addEventListener('submit', (e) => {
    e.preventDefault();
    saveWarehouseMerma();
  });

  $('mProduct').addEventListener('change', refreshWarehouseStockHint);
  $('mWarehouse').addEventListener('change', refreshWarehouseStockHint);


  if ($('dispatchMailboxDate')) $('dispatchMailboxDate').value = '';
  document.querySelectorAll('[data-dispatch-folder]').forEach(btn => {
    btn.addEventListener('click', () => {
      dispatchMailboxFolder = btn.dataset.dispatchFolder || 'sales';
      document.querySelectorAll('[data-dispatch-folder]').forEach(x => x.classList.toggle('active', x === btn));
      renderDispatchMailboxList();
    });
  });
  if ($('btnDispatchToday')) $('btnDispatchToday').addEventListener('click', async () => {
    dispatchMailboxAllDates = false;
    $('dispatchMailboxDate').value = operationalDateForForms();
    await loadDispatchMailbox();
  });
  if ($('btnDispatchAll')) {
    $('btnDispatchAll').classList.add('active');
    $('btnDispatchAll').addEventListener('click', async () => {
      dispatchMailboxAllDates = true;
      $('dispatchMailboxDate').value = '';
      await loadDispatchMailbox();
    });
  }
  if ($('dispatchMailboxDate')) $('dispatchMailboxDate').addEventListener('change', async () => {
    dispatchMailboxAllDates = false;
    await loadDispatchMailbox();
  });
  if ($('btnDispatchReload')) $('btnDispatchReload').addEventListener('click', loadDispatchMailbox);
  if ($('dispatchMailboxItems')) $('dispatchMailboxItems').addEventListener('click', (e) => {
    const item = e.target.closest('[data-dispatch-index]');
    if (item) selectDispatchMailboxRow(parseInt(item.dataset.dispatchIndex || '0', 10));
  });
  if ($('dispatchMailboxDetail')) $('dispatchMailboxDetail').addEventListener('click', (e) => {
    const vh = e.target.closest('[data-dispatch-open-vh]');
    if (vh) openSalesOrderDetail(parseInt(vh.dataset.dispatchOpenVh || '0', 10));
    const sales = e.target.closest('[data-dispatch-open-sales]');
    if (sales) openBreportSalesOrderDetail(parseInt(sales.dataset.dispatchOpenSales || '0', 10));
  });

  document.querySelectorAll('.operation-nav-btn[data-ops-section]').forEach(btn => {
    btn.addEventListener('click', () => showModuleSection(btn.dataset.opsSection));
  });

  if ($('btnHistoryRefresh')) $('btnHistoryRefresh').addEventListener('click', async () => {
    mermaPage = 1;
    await loadInventoryForSelectedDates();
    await loadMovementSummary();
    renderProductCards();
    await loadWarehouseMermas(1);
  });
  if ($('btnExportMovements')) $('btnExportMovements').addEventListener('click', () => {
    const dateFrom = $('histDateFrom')?.value || getMonthRange().from;
    const dateTo = $('histDateTo')?.value || getMonthRange().to;
    window.location.href = `?action=moves_export_csv&date_from=${encodeURIComponent(dateFrom)}&date_to=${encodeURIComponent(dateTo)}&direction=ALL`;
  });
  ['historyInCards','historyOutCards'].forEach(id => {
    if ($(id)) $(id).addEventListener('click', (e) => {
      const card = e.target.closest('[data-move-product]');
      if (!card) return;
      openMovementHistory(card.dataset.moveProduct, card.dataset.moveDirection || 'ALL', 1);
    });
  });
  if ($('btnMovementHistorySearch')) $('btnMovementHistorySearch').addEventListener('click', () => loadMovementHistoryPage(1));

  document.querySelectorAll('[data-locreq-mode]').forEach(btn => {
    btn.addEventListener('click', () => {
      document.querySelectorAll('[data-locreq-mode]').forEach(x => x.classList.remove('active'));
      btn.classList.add('active');
      loadLocRequests(btn.dataset.locreqMode || 'received');
    });
  });

  if ($('btnLoadSalesRoutes')) $('btnLoadSalesRoutes').addEventListener('click', loadSalesCreatedOrders);
  if ($('salesRouteDate')) $('salesRouteDate').addEventListener('change', loadSalesCreatedOrders);
  if ($('salesRouteCards')) $('salesRouteCards').addEventListener('click', (e) => {
    const card = e.target.closest('[data-route-card]');
    if (!card) return;
    openRouteOrders(card.dataset.routeCard || '0');
  });

  document.addEventListener('click', (e) => {
    const bDel = e.target.closest('[data-del]');
    if (bDel) {
      const id = parseInt(bDel.dataset.del || '0', 10);
      if (id > 0) delMove(id);
    }

    const bSales = e.target.closest('[data-sales-detail]');
    if (bSales) {
      const id = parseInt(bSales.dataset.salesDetail || '0', 10);
      if (id > 0) openSalesOrderDetail(id);
    }

    const bBreportSales = e.target.closest('[data-breport-sales-detail]');
    if (bBreportSales) {
      const id = parseInt(bBreportSales.dataset.breportSalesDetail || '0', 10);
      if (id > 0) { if (routeOrdersModal) routeOrdersModal.hide(); openBreportSalesOrderDetail(id); }
    }

    const bLocReq = e.target.closest('[data-locreq-detail]');
    if (bLocReq) {
      const id = parseInt(bLocReq.dataset.locreqDetail || '0', 10);
      if (id > 0) openLocReqDetail(id);
    }
  });
});
</script>
</body>
</html>