<?php
declare(strict_types=1);
/**
 * BReport · Carga camión Botella · v2 rediseño OC/dirección
 * ------------------------------------------------------------
 * Planeación visual de carga por pedido.
 * No descuenta inventario; solo actualiza artículos del pedido y acomodo.
 */
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/helpers.php';
require_once __DIR__ . '/includes/csrf.php';

require_scope();
ensure_session();

$pdo = pdo();
$u = current_user();
$sc = scope();
$companyId = (int)($sc['company_id'] ?? 0);
$locationId = (int)($sc['location_id'] ?? 0);
$action = (string)($_GET['action'] ?? '');

function truck_h(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function truck_jexit(array $d, int $code=200): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); echo json_encode($d, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); exit; }
function truck_read_json(): array { $raw=file_get_contents('php://input'); $d=json_decode($raw?:'[]', true); return is_array($d)?$d:[]; }
function truck_need_csrf(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') return; $hdr=(string)($_SERVER['HTTP_X_CSRF'] ?? ''); if($hdr==='') truck_jexit(['ok'=>false,'error'=>'Falta encabezado X-CSRF'],419); csrf_verify($hdr); }
function truck_table_exists(PDO $pdo, string $table): bool { $st=$pdo->prepare("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=?"); $st->execute([$table]); return (int)$st->fetchColumn()>0; }
function truck_col_exists(PDO $pdo, string $table, string $col): bool { $st=$pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=? AND column_name=?"); $st->execute([$table,$col]); return (int)$st->fetchColumn()>0; }
function truck_add_col(PDO $pdo, string $table, string $col, string $def): void { if(!truck_col_exists($pdo,$table,$col)) { try{$pdo->exec("ALTER TABLE `$table` ADD COLUMN $def");}catch(Throwable $e){} } }
function truck_scope_is_botella(PDO $pdo, int $companyId, int $locationId): bool { $st=$pdo->prepare("SELECT CONCAT(COALESCE(c.name,''),' ',COALESCE(l.name,'')) FROM companies c JOIN locations l ON l.company_id=c.id AND l.id=? WHERE c.id=? LIMIT 1"); $st->execute([$locationId,$companyId]); return str_contains(mb_strtoupper((string)$st->fetchColumn(),'UTF-8'), 'BOTELLA'); }
function truck_vehicle_capacity(string $vehicleType): array { $v=mb_strtoupper(trim($vehicleType),'UTF-8'); if(str_contains($v,'TORTON')) return ['vehicle_type'=>'TORTON','vehicle_label'=>'Torton','capacity'=>18,'cols'=>9]; return ['vehicle_type'=>'CAJA_SECA','vehicle_label'=>'Caja seca','capacity'=>30,'cols'=>15]; }
function truck_normalize_vehicle(string $text): string { return str_contains(mb_strtoupper($text,'UTF-8'),'TORTON') ? 'TORTON' : 'CAJA_SECA'; }

function truck_ensure_schema(PDO $pdo): void {
    if(!truck_table_exists($pdo,'sales_truck_load_plans')) {
        $pdo->exec("CREATE TABLE sales_truck_load_plans (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            location_id BIGINT UNSIGNED NOT NULL,
            order_id BIGINT UNSIGNED NOT NULL,
            vehicle_type VARCHAR(30) NOT NULL DEFAULT 'CAJA_SECA',
            vehicle_label VARCHAR(80) NOT NULL DEFAULT 'Caja seca',
            capacity_pallets INT NOT NULL DEFAULT 30,
            status VARCHAR(30) NOT NULL DEFAULT 'SAVED',
            notes VARCHAR(255) DEFAULT NULL,
            created_by BIGINT UNSIGNED DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_by BIGINT UNSIGNED DEFAULT NULL,
            updated_at DATETIME DEFAULT NULL,
            PRIMARY KEY(id),
            UNIQUE KEY uq_stlp_order(company_id, location_id, order_id),
            KEY idx_stlp_scope(company_id, location_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    } else {
        truck_add_col($pdo,'sales_truck_load_plans','vehicle_label',"vehicle_label VARCHAR(80) NOT NULL DEFAULT 'Caja seca' AFTER vehicle_type");
        truck_add_col($pdo,'sales_truck_load_plans','capacity_pallets',"capacity_pallets INT NOT NULL DEFAULT 30 AFTER vehicle_label");
        truck_add_col($pdo,'sales_truck_load_plans','updated_by',"updated_by BIGINT UNSIGNED DEFAULT NULL AFTER created_at");
        truck_add_col($pdo,'sales_truck_load_plans','updated_at',"updated_at DATETIME DEFAULT NULL AFTER updated_by");
        try{$pdo->exec("ALTER TABLE sales_truck_load_plans ADD UNIQUE KEY uq_stlp_order(company_id, location_id, order_id)");}catch(Throwable $e){}
    }
    if(!truck_table_exists($pdo,'sales_truck_load_plan_slots')) {
        $pdo->exec("CREATE TABLE sales_truck_load_plan_slots (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            plan_id BIGINT UNSIGNED NOT NULL,
            slot_no INT NOT NULL,
            order_item_id BIGINT UNSIGNED DEFAULT NULL,
            product_id BIGINT UNSIGNED NOT NULL,
            purchase_order VARCHAR(80) DEFAULT NULL,
            product_label VARCHAR(180) DEFAULT NULL,
            pallets_qty DECIMAL(14,3) NOT NULL DEFAULT 1.000,
            packages_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            pieces_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            notes VARCHAR(255) DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY(id),
            KEY idx_stlps_plan(plan_id),
            KEY idx_stlps_product(product_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }
    // Refuerzo para registrar a qué cliente/dirección corresponde cada OC cargada en el camión.
    truck_add_col($pdo,'sales_truck_load_plan_slots','customer_id',"customer_id BIGINT UNSIGNED DEFAULT NULL AFTER product_id");
    truck_add_col($pdo,'sales_truck_load_plan_slots','address_id',"address_id BIGINT UNSIGNED DEFAULT NULL AFTER customer_id");
    truck_add_col($pdo,'sales_truck_load_plan_slots','customer_label',"customer_label VARCHAR(180) DEFAULT NULL AFTER address_id");
    truck_add_col($pdo,'sales_truck_load_plan_slots','delivery_address',"delivery_address VARCHAR(255) DEFAULT NULL AFTER customer_label");
    if(truck_table_exists($pdo,'sales_orders') && !truck_col_exists($pdo,'sales_orders','address_id')) {
        truck_add_col($pdo,'sales_orders','address_id',"address_id BIGINT UNSIGNED DEFAULT NULL AFTER customer_user_id");
    }
}
function truck_get_order(PDO $pdo, int $companyId, int $locationId, int $orderId): ?array {
    $st=$pdo->prepare("SELECT o.*, c.business_name, c.contact_name, c.customer_code, l.name AS location_name, l.code AS location_code
        FROM sales_orders o JOIN sales_customers c ON c.id=o.customer_id LEFT JOIN locations l ON l.id=o.location_id
        WHERE o.id=? AND o.company_id=? AND o.location_id=? LIMIT 1");
    $st->execute([$orderId,$companyId,$locationId]); $r=$st->fetch(PDO::FETCH_ASSOC); return $r?:null;
}
function truck_current_plan(PDO $pdo, int $companyId, int $locationId, int $orderId): ?array { $st=$pdo->prepare("SELECT * FROM sales_truck_load_plans WHERE company_id=? AND location_id=? AND order_id=? LIMIT 1"); $st->execute([$companyId,$locationId,$orderId]); $r=$st->fetch(PDO::FETCH_ASSOC); return $r?:null; }
function truck_plan_slots(PDO $pdo, int $planId): array { $st=$pdo->prepare("SELECT * FROM sales_truck_load_plan_slots WHERE plan_id=? ORDER BY slot_no"); $st->execute([$planId]); return $st->fetchAll(PDO::FETCH_ASSOC); }
function truck_packaging(PDO $pdo, int $companyId, int $productId, int $packagingId): array {
    if($packagingId>0 && truck_table_exists($pdo,'product_packaging_options')) {
        $st=$pdo->prepare("SELECT id, product_id, packaging_type, packaging_name, pieces_per_package, packages_per_pallet, active, 0 AS legacy_fallback FROM product_packaging_options WHERE id=? AND company_id=? AND product_id=? AND active=1 LIMIT 1");
        $st->execute([$packagingId,$companyId,$productId]); $r=$st->fetch(PDO::FETCH_ASSOC); if($r) return $r;
    }
    $st=$pdo->prepare("SELECT 0 AS id, id AS product_id, 'TARIMA' AS packaging_type, CONCAT('Embalaje principal · ', packages_per_pallet, ' paq/tar') AS packaging_name, pieces_per_package, packages_per_pallet, active, 1 AS legacy_fallback FROM products WHERE id=? AND company_id=? AND active=1 AND COALESCE(pieces_per_package,0)>0 AND COALESCE(packages_per_pallet,0)>0 LIMIT 1");
    $st->execute([$productId,$companyId]); $r=$st->fetch(PDO::FETCH_ASSOC); if(!$r) throw new RuntimeException('El producto no tiene embalaje configurado.'); return $r;
}
function truck_packaging_options(PDO $pdo, int $companyId, int $productId): array {
    $rows=[];
    if(truck_table_exists($pdo,'product_packaging_options')) {
        $st=$pdo->prepare("SELECT id, product_id, packaging_type, packaging_name, pieces_per_package, packages_per_pallet, is_default, 0 AS legacy_fallback FROM product_packaging_options WHERE company_id=? AND product_id=? AND active=1 ORDER BY is_default DESC, packages_per_pallet DESC, id");
        $st->execute([$companyId,$productId]); $rows=$st->fetchAll(PDO::FETCH_ASSOC);
    }
    if(!$rows) { $r=truck_packaging($pdo,$companyId,$productId,0); $r['is_default']=1; $rows[]=$r; }
    foreach($rows as &$r) { $r['pieces_per_pallet']=(int)$r['pieces_per_package']*(int)$r['packages_per_pallet']; }
    unset($r); return $rows;
}
function truck_products(PDO $pdo, int $companyId): array { $st=$pdo->prepare("SELECT id, sku, name, uom, pieces_per_package, packages_per_pallet FROM products WHERE company_id=? AND active=1 ORDER BY sku, name"); $st->execute([$companyId]); return $st->fetchAll(PDO::FETCH_ASSOC); }
function truck_order_items(PDO $pdo, int $orderId): array {
    $st=$pdo->prepare("SELECT i.id AS order_item_id, i.product_id, i.packaging_option_id, i.qty, i.packages_qty, i.pallets_qty, i.pallet_type, i.pallet_specification, i.notes, p.sku, p.name AS product_name, p.uom, COALESCE(po.packaging_name, CONCAT('Embalaje · ', p.packages_per_pallet, ' paq/tar')) AS packaging_label, COALESCE(po.pieces_per_package,p.pieces_per_package) AS pieces_per_package, COALESCE(po.packages_per_pallet,p.packages_per_pallet) AS packages_per_pallet, COALESCE(po.pieces_per_package,p.pieces_per_package)*COALESCE(po.packages_per_pallet,p.packages_per_pallet) AS pieces_per_pallet
        FROM sales_order_items i JOIN products p ON p.id=i.product_id LEFT JOIN product_packaging_options po ON po.id=i.packaging_option_id
        WHERE i.order_id=? ORDER BY i.id");
    $st->execute([$orderId]); return $st->fetchAll(PDO::FETCH_ASSOC);
}
function truck_orders(PDO $pdo, int $companyId, int $locationId): array {
    /*
     * Carga camión debe mostrar únicamente lo pendiente por surtir.
     * Cuando Almacén confirme salida (outbound_id) o el pedido avance a surtido/ruta/entregado,
     * deja de aparecer para evitar acumulación histórica.
     */
    $st=$pdo->prepare("SELECT o.id, o.sales_order_ref, o.purchase_order, o.vehicle_unit, o.requested_delivery_date, o.requested_delivery_time, o.route_no, o.status, o.delivery_address, c.business_name, c.contact_name, COALESCE(SUM(soi.pallets_qty),0) AS total_pallets, COUNT(soi.id) AS items_count, lp.id AS plan_id, lp.vehicle_type AS plan_vehicle_type
        FROM sales_orders o JOIN sales_customers c ON c.id=o.customer_id LEFT JOIN sales_order_items soi ON soi.order_id=o.id LEFT JOIN sales_truck_load_plans lp ON lp.order_id=o.id AND lp.company_id=o.company_id AND lp.location_id=o.location_id
        WHERE o.company_id=? AND o.location_id=?
          AND o.outbound_id IS NULL
          AND UPPER(o.status) NOT IN ('CANCELADA','RECHAZADA','SURTIENDO','SURTIDA','EN_RUTA','ENTREGADA')
        GROUP BY o.id, o.sales_order_ref, o.purchase_order, o.vehicle_unit, o.requested_delivery_date, o.requested_delivery_time, o.route_no, o.status, o.delivery_address, c.business_name, c.contact_name, lp.id, lp.vehicle_type
        ORDER BY o.requested_delivery_date ASC, o.id DESC LIMIT 300");
    $st->execute([$companyId,$locationId]); return $st->fetchAll(PDO::FETCH_ASSOC);
}

function truck_customer_addresses(PDO $pdo, int $companyId, int $customerId, int $locationId): array {
    if($customerId<=0 || !truck_table_exists($pdo,'sales_customer_addresses')) return [];
    $hasLocation = truck_col_exists($pdo,'sales_customer_addresses','location_id');
    $sql="SELECT id, address, postal_code, is_default FROM sales_customer_addresses WHERE customer_id=? AND active=1";
    $params=[$customerId];
    if($hasLocation){ $sql .= " AND (location_id IS NULL OR location_id=?)"; $params[]=$locationId; }
    $sql .= " ORDER BY is_default DESC, id DESC";
    $st=$pdo->prepare($sql); $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC);
}

function truck_customers(PDO $pdo, int $companyId, int $locationId): array {
    if(!truck_table_exists($pdo,'sales_customers')) return [];
    $hasLocation = truck_col_exists($pdo,'sales_customers','location_id');
    $codeCol = truck_col_exists($pdo,'sales_customers','customer_code') ? 'customer_code' : "NULL AS customer_code";
    $sql = "SELECT id, {$codeCol}, business_name, contact_name" . ($hasLocation ? ", location_id" : ", NULL AS location_id") . " FROM sales_customers WHERE company_id=? AND active=1";
    $params = [$companyId];
    if($hasLocation){ $sql .= " AND (location_id IS NULL OR location_id=?)"; $params[] = $locationId; }
    $sql .= " ORDER BY business_name, contact_name, id";
    $st=$pdo->prepare($sql); $st->execute($params); return $st->fetchAll(PDO::FETCH_ASSOC);
}

function truck_oc_context(PDO $pdo, int $companyId, int $locationId, int $baseOrderId, string $purchaseOrder): array {
    $base = truck_get_order($pdo,$companyId,$locationId,$baseOrderId);
    if(!$base) throw new RuntimeException('Selecciona una orden base válida.');
    $purchaseOrder = trim($purchaseOrder);
    $match = null;
    if($purchaseOrder !== ''){
        $st=$pdo->prepare("SELECT o.*, c.business_name, c.contact_name, c.customer_code
            FROM sales_orders o JOIN sales_customers c ON c.id=o.customer_id
            WHERE o.company_id=? AND o.location_id=? AND TRIM(o.purchase_order)=?
              AND o.status NOT IN ('CANCELADA','RECHAZADA')
            ORDER BY o.id DESC LIMIT 1");
        $st->execute([$companyId,$locationId,$purchaseOrder]);
        $match=$st->fetch(PDO::FETCH_ASSOC) ?: null;
    }
    $ctx = $match ?: $base;
    $customerId = (int)($ctx['customer_id'] ?? 0);
    return [
        'base_order' => [
            'id'=>(int)$base['id'],
            'purchase_order'=>(string)($base['purchase_order']??''),
            'customer_id'=>(int)($base['customer_id']??0),
            'customer_label'=>trim((string)($base['business_name'] ?: $base['contact_name'])),
            'address_id'=>(int)($base['address_id']??0),
            'delivery_address'=>(string)($base['delivery_address']??''),
        ],
        'matched' => $match ? 1 : 0,
        'oc_order' => [
            'id'=>(int)($ctx['id']??0),
            'purchase_order'=>$purchaseOrder,
            'customer_id'=>$customerId,
            'customer_label'=>trim((string)(($ctx['business_name']??'') ?: ($ctx['contact_name']??''))),
            'customer_code'=>(string)($ctx['customer_code']??''),
            'address_id'=>(int)($ctx['address_id']??0),
            'delivery_address'=>(string)($ctx['delivery_address']??''),
            'same_customer'=>((int)($ctx['customer_id']??0) === (int)($base['customer_id']??0)) ? 1 : 0,
        ],
        'addresses'=>truck_customer_addresses($pdo,$companyId,$customerId,$locationId),
        'customers'=>truck_customers($pdo,$companyId,$locationId),
    ];
}

function truck_save_plan(PDO $pdo, int $companyId, int $locationId, int $userId, array $payload): array {
    $orderId=(int)($payload['order_id']??0); if($orderId<=0) throw new RuntimeException('Selecciona una orden de venta.');
    $order=truck_get_order($pdo,$companyId,$locationId,$orderId); if(!$order) throw new RuntimeException('Orden no encontrada para esta planta.');
    if((int)($order['outbound_id']??0)>0) throw new RuntimeException('No se puede modificar carga de una orden con salida confirmada.');
    $vehicle=truck_vehicle_capacity((string)($payload['vehicle_type']??'CAJA_SECA')); $capacity=(int)$vehicle['capacity'];
    $slotNo=$capacity; // Carga de adentro hacia afuera: inicia junto a la cabina (30/18) y avanza hacia la puerta.
    $items=$payload['items']??[]; if(!is_array($items)||!$items) throw new RuntimeException('Agrega al menos un artículo a la carga.');
    $clean=[]; $slots=[]; $totalPallets=0;
    foreach($items as $row) {
        $productId=(int)($row['product_id']??0); $packagingId=(int)($row['packaging_option_id']??0); $palletsRaw=(float)($row['pallets_to_load']??0);
        if($productId<=0) throw new RuntimeException('Selecciona producto en todos los renglones.');
        if($palletsRaw<=0) continue;
        if(abs($palletsRaw-round($palletsRaw))>0.000001) throw new RuntimeException('La carga del camión solo permite tarimas enteras.');
        $pallets=(int)round($palletsRaw); $totalPallets += $pallets;
        if($totalPallets>$capacity) throw new RuntimeException('La carga excede la capacidad de '.$vehicle['vehicle_label'].' ('.$capacity.' tarimas).');
        $stP=$pdo->prepare("SELECT id, sku, name, uom FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1"); $stP->execute([$productId,$companyId]); $p=$stP->fetch(PDO::FETCH_ASSOC); if(!$p) throw new RuntimeException('Producto inválido o inactivo.');
        $pkg=truck_packaging($pdo,$companyId,$productId,$packagingId); $ppp=(int)$pkg['packages_per_pallet']; $piecesPack=(int)$pkg['pieces_per_package']; if($ppp<=0||$piecesPack<=0) throw new RuntimeException('Embalaje inválido para '.$p['sku']);
        $packages=$pallets*$ppp; $pieces=$packages*$piecesPack;
        $po=trim((string)($row['purchase_order']??($order['purchase_order']??'')));
        $palletType=trim((string)($row['pallet_type']??'')); $palletSpec=trim((string)($row['pallet_specification']??''));
        $label=trim((string)$p['sku'].' · '.(string)$p['name']);
        $rowCustomerId=(int)($row['customer_id']??($order['customer_id']??0));
        $rowAddressId=(int)($row['address_id']??($order['address_id']??0));
        $rowCustomerLabel=trim((string)($row['customer_label']??($order['business_name'] ?? $order['contact_name'] ?? '')));
        $rowDeliveryAddress=trim((string)($row['delivery_address']??($order['delivery_address']??'')));
        $clean[]=['product_id'=>$productId,'packaging_option_id'=>$packagingId>0?$packagingId:null,'qty'=>$pieces,'packages_qty'=>$packages,'pallets_qty'=>$pallets,'pallet_type'=>$palletType?:null,'pallet_specification'=>$palletSpec?:null,'notes'=>'Carga camión: '.$vehicle['vehicle_label'].' | '.$pallets.' tarimas | '.$packages.' paquetes | '.$pieces.' piezas'];
        for($i=0;$i<$pallets;$i++) {
            $slots[]=['slot_no'=>$slotNo--,'product_id'=>$productId,'purchase_order'=>$po?:null,'product_label'=>$label,'packages_qty'=>$ppp,'pieces_qty'=>$ppp*$piecesPack,'customer_id'=>$rowCustomerId>0?$rowCustomerId:null,'address_id'=>$rowAddressId>0?$rowAddressId:null,'customer_label'=>$rowCustomerLabel?:null,'delivery_address'=>$rowDeliveryAddress?:null];
        }
    }
    if(!$clean) throw new RuntimeException('Captura al menos una tarima para cargar.');
    $pdo->beginTransaction();
    try{
        $pdo->prepare("DELETE FROM sales_order_items WHERE order_id=?")->execute([$orderId]);
        $inserted=[];
        $stItem=$pdo->prepare("INSERT INTO sales_order_items(order_id, product_id, packaging_option_id, qty, packages_qty, pallets_qty, pallet_type, pallet_specification, unit_price, line_total, notes, created_at) VALUES(?,?,?,?,?,?,?,?,0,0,?,NOW())");
        foreach($clean as $it){ $stItem->execute([$orderId,$it['product_id'],$it['packaging_option_id'],$it['qty'],$it['packages_qty'],$it['pallets_qty'],$it['pallet_type'],$it['pallet_specification'],$it['notes']]); $inserted[]=(int)$pdo->lastInsertId(); }
        $pdo->prepare("UPDATE sales_orders SET vehicle_unit=?, updated_at=NOW() WHERE id=? AND company_id=? AND location_id=?")->execute([$vehicle['vehicle_label'],$orderId,$companyId,$locationId]);
        $existing=truck_current_plan($pdo,$companyId,$locationId,$orderId);
        if($existing){ $planId=(int)$existing['id']; $pdo->prepare("UPDATE sales_truck_load_plans SET vehicle_type=?, vehicle_label=?, capacity_pallets=?, notes=?, updated_by=?, updated_at=NOW() WHERE id=?")->execute([$vehicle['vehicle_type'],$vehicle['vehicle_label'],$capacity,trim((string)($payload['notes']??''))?:null,$userId,$planId]); $pdo->prepare("DELETE FROM sales_truck_load_plan_slots WHERE plan_id=?")->execute([$planId]); }
        else { $pdo->prepare("INSERT INTO sales_truck_load_plans(company_id, location_id, order_id, vehicle_type, vehicle_label, capacity_pallets, status, notes, created_by, created_at, updated_at) VALUES(?,?,?,?,?,?,'SAVED',?,?,NOW(),NOW())")->execute([$companyId,$locationId,$orderId,$vehicle['vehicle_type'],$vehicle['vehicle_label'],$capacity,trim((string)($payload['notes']??''))?:null,$userId]); $planId=(int)$pdo->lastInsertId(); }
        $stSlot=$pdo->prepare("INSERT INTO sales_truck_load_plan_slots(plan_id, slot_no, order_item_id, product_id, customer_id, address_id, customer_label, delivery_address, purchase_order, product_label, pallets_qty, packages_qty, pieces_qty, created_at) VALUES(?,?,?,?,?,?,?,?,?,?,1,?,?,NOW())");
        // Relaciona cada slot con el order_item correspondiente por acumulado.
        $itemIndex=0; $left=(float)($clean[0]['pallets_qty']??0); foreach($slots as $s){ while($left<=0 && $itemIndex<count($inserted)-1){$itemIndex++; $left=(float)$clean[$itemIndex]['pallets_qty'];} $stSlot->execute([$planId,$s['slot_no'],$inserted[$itemIndex]??null,$s['product_id'],$s['customer_id']??null,$s['address_id']??null,$s['customer_label']??null,$s['delivery_address']??null,$s['purchase_order'],$s['product_label'],$s['packages_qty'],$s['pieces_qty']]); $left--; }
        $pdo->commit(); return ['plan_id'=>$planId,'slots_count'=>count($slots),'capacity'=>$capacity,'free_spaces'=>max(0,$capacity-count($slots)),'vehicle_label'=>$vehicle['vehicle_label']];
    }catch(Throwable $e){ if($pdo->inTransaction()) $pdo->rollBack(); throw $e; }
}

truck_ensure_schema($pdo);
if(!truck_scope_is_botella($pdo,$companyId,$locationId)){ http_response_code(403); exit('Esta sección corresponde únicamente a Botella.'); }
$canManage = has_role('admin') || has_role('sales') || has_role('ventas') || has_role('supervisor') || has_role('corp') || has_role('plant_admin');
if(!$canManage){ http_response_code(403); exit('Acceso denegado.'); }

if($action==='print'){
    $planId=(int)($_GET['plan_id']??0);
    $st=$pdo->prepare("SELECT lp.*, o.sales_order_ref, o.purchase_order, o.requested_delivery_date, o.requested_delivery_time, o.route_no, o.delivery_address, o.vehicle_unit, c.business_name, c.contact_name FROM sales_truck_load_plans lp JOIN sales_orders o ON o.id=lp.order_id JOIN sales_customers c ON c.id=o.customer_id WHERE lp.id=? AND lp.company_id=? AND lp.location_id=? LIMIT 1");
    $st->execute([$planId,$companyId,$locationId]);
    $plan=$st->fetch(PDO::FETCH_ASSOC);
    if(!$plan) exit('Plan no encontrado.');

    $slots=truck_plan_slots($pdo,$planId);
    $vehicle=truck_vehicle_capacity((string)$plan['vehicle_type']);
    $capacity = (int)$plan['capacity_pallets'];

    $displayOrder = [];
    for($i=1; $i<=$capacity; $i+=2) $displayOrder[] = $i;
    for($i=2; $i<=$capacity; $i+=2) $displayOrder[] = $i;

    $orderItemIds = array_values(array_unique(array_filter(array_map(fn($s)=>(int)($s['order_item_id']??0), $slots))));
    $itemMap = [];
    if($orderItemIds){
        $ph = implode(',', array_fill(0, count($orderItemIds), '?'));
        $stItems = $pdo->prepare("SELECT i.id, i.product_id, i.packaging_option_id, i.pallet_type, i.pallet_specification, i.packages_qty, i.pallets_qty, i.qty,
                   p.sku, p.name AS product_name,
                   COALESCE(po.packaging_name, CONCAT('Embalaje · ', p.packages_per_pallet, ' paq/tar')) AS packaging_label,
                   COALESCE(po.packages_per_pallet,p.packages_per_pallet) AS packages_per_pallet,
                   COALESCE(po.pieces_per_package,p.pieces_per_package) AS pieces_per_package
            FROM sales_order_items i
            JOIN products p ON p.id=i.product_id
            LEFT JOIN product_packaging_options po ON po.id=i.packaging_option_id
            WHERE i.id IN ($ph)");
        $stItems->execute($orderItemIds);
        foreach($stItems->fetchAll(PDO::FETCH_ASSOC) as $it){
            $itemMap[(int)$it['id']] = $it;
        }
    }

    $summary = [];
    foreach($slots as $s){
        $itemId = (int)($s['order_item_id'] ?? 0);
        $item = $itemMap[$itemId] ?? [];
        $po = trim((string)($s['purchase_order'] ?? $plan['purchase_order'] ?? ''));
        $productId = (int)($s['product_id'] ?? 0);
        $key = $po . '|' . $productId . '|' . $itemId;
        if(!isset($summary[$key])){
            $productLabel = trim((string)($s['product_label'] ?? ''));
            if($productLabel === '') $productLabel = trim((string)($item['sku'] ?? '') . ' · ' . (string)($item['product_name'] ?? ''));
            $summary[$key] = [
                'fecha' => date('d/m/Y'),
                'presentacion' => $productLabel,
                'packages_per_pallet' => (int)($item['packages_per_pallet'] ?? $s['packages_qty'] ?? 0),
                'total_packages' => 0,
                'total_spaces' => 0,
                'total_pieces' => 0,
                'cliente' => (string)(($s['customer_label'] ?? '') ?: ($plan['business_name'] ?: $plan['contact_name'])),
                'destino' => (string)(($s['delivery_address'] ?? '') ?: ($plan['delivery_address'] ?? '')),
                'fecha_entrega' => (string)($plan['requested_delivery_date'] ?? ''),
                'hora_entrega' => (string)($plan['requested_delivery_time'] ?? ''),
                'orden_compra' => $po,
                'unidad' => (string)($plan['vehicle_label'] ?? ''),
                'tipo_tarima' => (string)($item['pallet_type'] ?? ''),
                'acomodo' => [],
                'ruta' => (string)($plan['route_no'] ?? ''),
            ];
        }
        $summary[$key]['total_spaces'] += 1;
        $summary[$key]['total_packages'] += (float)($s['packages_qty'] ?? 0);
        $summary[$key]['total_pieces'] += (float)($s['pieces_qty'] ?? 0);
        $summary[$key]['acomodo'][] = (int)($s['slot_no'] ?? 0);
    }
    $summaryRows = array_values($summary);

    $map=[];
    foreach($slots as $s){$map[(int)$s['slot_no']]=$s;}
    $ocColorMap=[];
    foreach($slots as $s){
        $ocKey=mb_strtoupper(trim((string)($s['purchase_order']??'SIN_OC')),'UTF-8');
        if($ocKey==='') $ocKey='SIN_OC';
        if(!isset($ocColorMap[$ocKey])) $ocColorMap[$ocKey]='oc-color-'.(count($ocColorMap)%10);
    }
    ?><!doctype html><html lang="es"><head><meta charset="utf-8"><title>Carga camión <?=truck_h($plan['sales_order_ref'])?></title><style>
    body{font-family:Arial,sans-serif;margin:14px;color:#111;background:#fff}.logo{text-align:center;font-size:34px;color:#0d4e9b;font-weight:800;font-style:italic;line-height:1}.sub{text-align:center;letter-spacing:8px;color:#0d4e9b;font-weight:700;margin-bottom:14px}.tbl{width:100%;border-collapse:collapse;margin:8px 0 16px}.tbl th{background:#0877bd;color:white}.tbl th,.tbl td{border:1px solid #222;padding:5px;font-size:11px;vertical-align:middle}.tbl .num{text-align:right;white-space:nowrap}.tbl .smalltxt{font-size:10px;line-height:1.15}.tbl .slots{font-size:10px;max-width:120px;word-break:break-word}.truck-scene{display:flex;align-items:center;margin-top:20px;transform-origin:left top}.trailer{border:4px solid #9b9b9b;background:#f7f3e8;padding:12px;border-radius:8px;position:relative}.trailer-grid{display:grid;grid-template-columns:repeat(<?= (int)$vehicle['cols']?>,52px);grid-template-rows:repeat(2,52px);gap:7px}.slot{border:1px solid #b5823b;background:#d99b41;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:10px;font-weight:800;color:#261400;line-height:1.1}.slot.oc-color-0{background:#d99b41}.slot.oc-color-1{background:#60a5fa}.slot.oc-color-2{background:#34d399}.slot.oc-color-3{background:#f472b6}.slot.oc-color-4{background:#a78bfa}.slot.oc-color-5{background:#fbbf24}.slot.oc-color-6{background:#22d3ee}.slot.oc-color-7{background:#fb7185}.slot.oc-color-8{background:#84cc16}.slot.oc-color-9{background:#c084fc}.slot.empty{background:#f8fafc;color:#a0aec0;border:1px dashed #cbd5e1}.slot-no{font-size:14px;color:#111}.oc{font-size:9px;max-width:48px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.prod{font-size:8px;max-width:48px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.hitch{width:36px;height:14px;background:#777;margin:0 -2px}.cab{width:170px;height:100px;border:3px solid #bfc7d1;border-radius:22px 48px 48px 22px;background:linear-gradient(90deg,#f9fbff,#d7dde8);position:relative}.cab:before,.cab:after{content:'';position:absolute;width:34px;height:34px;border:3px solid #444;border-radius:50%;bottom:-18px;background:#222}.cab:before{left:30px}.cab:after{right:30px}.cab-logo{position:absolute;left:30px;top:36px;color:#0d4e9b;font-weight:900;font-size:18px;letter-spacing:1px;transform:rotate(-90deg)}.printbar{margin-bottom:10px}.legend{font-size:11px;margin-top:10px;color:#333}.section-title{font-weight:800;margin-top:8px;margin-bottom:4px}@media print{.printbar{display:none}body{margin:7mm}.truck-scene{transform:scale(.82)}}@page{size:landscape;margin:7mm}
    </style></head><body><div class="printbar"><button onclick="window.print()">Imprimir</button> <button onclick="window.close()">Cerrar</button></div><div class="logo">Blanquita</div><div class="sub">AGUA PURIFICADA</div>
    <div class="section-title">Resumen de carga por OC / artículo</div>
    <table class="tbl"><thead><tr><th>FECHA</th><th>PRESENTACIÓN</th><th>PAQUETES X TARIMA</th><th>TOTAL PAQUETES</th><th>TOTAL ESPACIOS</th><th>CLIENTE</th><th>DESTINO</th><th>FECHA DE ENTREGA</th><th>HORA DE ENTREGA</th><th>ORDEN DE COMPRA</th><th>UNIDAD A CARGAR</th><th>TIPO DE TARIMA</th><th>ACOMODO EN UNIDAD</th><th>RUTA A CARGAR DLX</th></tr></thead><tbody>
    <?php if(!$summaryRows): ?><tr><td colspan="14" style="text-align:center;">Sin artículos cargados.</td></tr><?php endif; ?>
    <?php foreach($summaryRows as $r): $slotsTxt = implode(', ', array_map('intval', $r['acomodo'])); ?>
      <tr><td><?=truck_h($r['fecha'])?></td><td class="smalltxt"><?=truck_h($r['presentacion'])?></td><td class="num"><?=number_format((float)$r['packages_per_pallet'],0,'.',',')?></td><td class="num"><?=number_format((float)$r['total_packages'],0,'.',',')?></td><td class="num"><?=number_format((float)$r['total_spaces'],0,'.',',')?></td><td><?=truck_h($r['cliente'])?></td><td class="smalltxt"><?=truck_h($r['destino'])?></td><td><?=truck_h($r['fecha_entrega'])?></td><td><?=truck_h($r['hora_entrega'])?></td><td><?=truck_h($r['orden_compra'])?></td><td><?=truck_h($r['unidad'])?></td><td><?=truck_h($r['tipo_tarima'])?></td><td class="slots"><?=truck_h($slotsTxt)?></td><td><?=truck_h($r['ruta'])?></td></tr>
    <?php endforeach; ?>
    </tbody></table>
    <div><strong><?=truck_h($plan['vehicle_label'])?> · <?=truck_h((string)$plan['capacity_pallets'])?> espacios · acomodo 2 filas · interior → exterior</strong></div><div class="truck-scene"><div class="trailer"><div class="trailer-grid"><?php foreach($displayOrder as $i): $s=$map[$i]??null; $ocClass=''; if($s){ $ocKey=mb_strtoupper(trim((string)($s['purchase_order']??'SIN_OC')),'UTF-8'); if($ocKey==='') $ocKey='SIN_OC'; $ocClass=$ocColorMap[$ocKey] ?? 'oc-color-0'; } ?><div class="slot <?=$s?$ocClass:'empty'?>"><div class="slot-no"><?=$i?></div><div class="oc"><?=truck_h($s['purchase_order']??'')?></div><div class="prod"><?=truck_h(mb_substr((string)($s['product_label']??''),0,14))?></div></div><?php endforeach; ?></div></div><div class="hitch"></div><div class="cab"><div class="cab-logo">CLIO</div></div></div><div class="legend">Cada cuadro representa un espacio de tarima. La carga se acomoda de adentro hacia afuera: inicia en el último espacio junto a cabina (30/18) y avanza hacia la puerta.</div></body></html><?php exit;
}
if($action!==''){
    truck_need_csrf();
    try{
        if($action==='bootstrap') truck_jexit(['ok'=>true,'orders'=>truck_orders($pdo,$companyId,$locationId),'products'=>truck_products($pdo,$companyId),'selected_order_id'=>(int)($_GET['order_id']??0)]);
        if($action==='packaging_options') { $pid=(int)($_GET['product_id']??0); truck_jexit(['ok'=>true,'rows'=>$pid>0?truck_packaging_options($pdo,$companyId,$pid):[]]); }
        if($action==='order_detail') { $oid=(int)($_GET['order_id']??0); $order=truck_get_order($pdo,$companyId,$locationId,$oid); if(!$order) truck_jexit(['ok'=>false,'error'=>'Orden no encontrada'],404); $plan=truck_current_plan($pdo,$companyId,$locationId,$oid); truck_jexit(['ok'=>true,'order'=>$order,'items'=>truck_order_items($pdo,$oid),'plan'=>$plan,'slots'=>$plan?truck_plan_slots($pdo,(int)$plan['id']):[],'suggested_vehicle'=>truck_normalize_vehicle((string)($order['vehicle_unit']??''))]); }
        if($action==='oc_context') { $oid=(int)($_GET['order_id']??0); $po=trim((string)($_GET['purchase_order']??'')); truck_jexit(['ok'=>true]+truck_oc_context($pdo,$companyId,$locationId,$oid,$po)); }
        if($action==='customer_addresses') { $cid=(int)($_GET['customer_id']??0); truck_jexit(['ok'=>true,'rows'=>truck_customer_addresses($pdo,$companyId,$cid,$locationId)]); }
        if($action==='save_plan') truck_jexit(['ok'=>true]+truck_save_plan($pdo,$companyId,$locationId,(int)($u['id']??0),truck_read_json()));
        truck_jexit(['ok'=>false,'error'=>'Acción no válida'],400);
    }catch(Throwable $e){ truck_jexit(['ok'=>false,'error'=>$e->getMessage()],400); }
}
$csrf=csrf_token(); $initialOrderId=(int)($_GET['order_id']??0);
?><!doctype html><html lang="es"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>BReport · Carga camión Botella</title><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.css" rel="stylesheet"><style>body{background:#eef3f8;color:#10243e}.topbar{background:linear-gradient(135deg,#102b63,#1d6ceb);color:white;padding:22px}.shell{max-width:1480px;margin:0 auto;padding:18px}.glass{background:white;border:1px solid #d8e3f0;border-radius:20px;box-shadow:0 14px 34px rgba(34,55,85,.10)}.panel-pad{padding:22px}.btn-primary-soft{background:#eef5ff;color:#0d6efd;border:1px solid #bfd7ff;font-weight:700}.btn-green-soft{background:#13a65a;color:white;border:0;font-weight:800}.muted{color:#64748b}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.truck-wrap{overflow:auto;padding:16px 0}.truck-scene{display:flex;align-items:center;min-width:max-content}.trailer-box{border:4px solid #94a3b8;background:#f8f3e8;padding:12px;border-radius:10px;position:relative;box-shadow:inset 0 0 0 2px #e2e8f0}.truck-grid{display:grid;grid-template-rows:repeat(2,74px);gap:9px;align-items:center;min-width:max-content}.slot{width:80px;height:70px;border:1px solid #b78237;background:#d69a44;border-radius:10px;padding:5px;text-align:center;font-size:11px;font-weight:800;color:#301b05;display:flex;flex-direction:column;justify-content:center;line-height:1.1}.slot.oc-color-0{background:#d69a44}.slot.oc-color-1{background:#60a5fa}.slot.oc-color-2{background:#34d399}.slot.oc-color-3{background:#f472b6}.slot.oc-color-4{background:#a78bfa}.slot.oc-color-5{background:#fbbf24}.slot.oc-color-6{background:#22d3ee}.slot.oc-color-7{background:#fb7185}.slot.oc-color-8{background:#84cc16}.slot.oc-color-9{background:#c084fc}.slot.empty{background:#f7fafc;color:#9aa9bb;border-style:dashed}.slot .slot-no{font-size:16px;color:#111}.slot .oc{font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.slot .prod{font-size:9px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.hitch-web{width:34px;height:16px;background:#64748b}.cab-web{width:180px;height:108px;border:3px solid #cbd5e1;border-radius:22px 52px 52px 22px;background:linear-gradient(90deg,#ffffff,#d9e1ec);position:relative;box-shadow:0 8px 18px rgba(15,23,42,.12)}.cab-web:before,.cab-web:after{content:'';position:absolute;width:34px;height:34px;border:3px solid #334155;border-radius:50%;bottom:-18px;background:#1f2937}.cab-web:before{left:34px}.cab-web:after{right:34px}.cab-logo-web{position:absolute;left:36px;top:39px;color:#0d4e9b;font-weight:900;font-size:20px;letter-spacing:1px;transform:rotate(-90deg)}.truck-head{font-weight:900;font-size:18px}.table-wrap{border:1px solid #dbe5f1;border-radius:16px;overflow:hidden}.table thead th{background:#eaf2ff}.oc-help{background:#f8fafc;border:1px dashed #cbd5e1;border-radius:14px;padding:10px}.order-active-box{background:#f8fafc;border:1px solid #dbe5f1;border-radius:14px;padding:12px}</style></head><body><div class="topbar"><div class="d-flex justify-content-between flex-wrap gap-2 align-items-center"><div><div class="small opacity-75">BReport · Ventas Botella</div><h1 class="m-0 fw-bold">Carga camión</h1><div class="opacity-75">Planeación visual de tarimas por unidad. Solo pedidos por surtir · OC por cliente/dirección · no descuenta inventario.</div></div><div class="d-flex gap-2"><a class="btn btn-light" href="ventas_botella.php"><i class="bi bi-arrow-left me-1"></i>Ventas</a><a class="btn btn-light" href="dashboard.php"><i class="bi bi-grid me-1"></i>Dashboard</a></div></div></div><div class="shell"><div class="row g-3"><div class="col-12 col-xl-4"><div class="glass panel-pad mb-3"><label class="form-label fw-bold">Orden por surtir</label><select id="orderId" class="form-select"></select><div class="small text-success fw-bold mt-2"><i class="bi bi-filter-circle me-1"></i>Solo se muestran pedidos pendientes de surtir. Cuando Almacén confirme salida, desaparecen de esta lista.</div><div id="orderInfo" class="mt-3 muted">Selecciona una orden.</div></div><div class="glass panel-pad"><div class="fw-bold mb-2"><i class="bi bi-box-seam text-primary"></i> Agregar artículo a la carga</div><div class="alert alert-light border small mb-3"><b>OC adicional:</b> cambia la OC antes de agregar el artículo. Si es diferente a la OC base, se abrirá un modal para confirmar cliente y punto de entrega.</div><div class="mb-2"><label class="form-label">Artículo</label><select id="productId" class="form-select"></select></div><div class="mb-2"><label class="form-label">Embalaje</label><select id="packagingId" class="form-select"></select></div><div class="row g-2"><div class="col-6"><label class="form-label">Tarimas</label><input id="pallets" type="number" min="1" step="1" class="form-control mono" value="1"></div><div class="col-6"><label class="form-label">OC por espacio</label><input id="po" class="form-control mono" placeholder="OC"></div><div class="col-6"><label class="form-label">Tipo tarima</label><select id="palletType" class="form-select"><option>Chep</option><option>Gris</option></select></div><div class="col-6"><label class="form-label">Especificación</label><select id="palletSpec" class="form-select"><option>retorno</option><option>surtido</option></select></div></div><div id="calcInfo" class="small muted mt-2"></div><button id="btnAdd" class="btn btn-primary-soft w-100 mt-3" type="button"><i class="bi bi-plus-circle me-1"></i>Agregar al camión</button></div></div><div class="col-12 col-xl-8"><div class="glass panel-pad mb-3"><div class="row g-3 align-items-end"><div class="col-12 col-md-4"><label class="form-label fw-bold">Unidad a cargar</label><select id="vehicleType" class="form-select"><option value="CAJA_SECA">Caja seca · 30 tarimas</option><option value="TORTON">Torton · 18 tarimas</option></select></div><div class="col-12 col-md-8"><label class="form-label fw-bold">Notas</label><input id="planNotes" class="form-control" placeholder="Observaciones para reparto / almacén"></div></div><div class="row g-2 mt-3"><div class="col-6 col-md-3"><div class="p-3 rounded-4 bg-light"><div class="small muted">Unidad</div><div class="fw-bold" id="kVehicle">Caja seca</div></div></div><div class="col-6 col-md-3"><div class="p-3 rounded-4 bg-light"><div class="small muted">Capacidad</div><div class="fw-bold"><span id="kCapacity">30</span></div></div></div><div class="col-6 col-md-3"><div class="p-3 rounded-4 bg-light"><div class="small muted">Ocupado</div><div class="fw-bold"><span id="kUsed">0</span></div></div></div><div class="col-6 col-md-3"><div class="p-3 rounded-4 bg-light"><div class="small muted">Disponible</div><div class="fw-bold"><span id="kFree">30</span></div></div></div></div></div><div class="glass panel-pad mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><div class="truck-head" id="trailerHead">Caja seca · 30 espacios</div><button id="btnClear" class="btn btn-outline-secondary btn-sm" type="button"><i class="bi bi-eraser me-1"></i>Limpiar carga</button></div><div class="truck-wrap"><div class="truck-scene"><div class="trailer-box"><div id="truckGrid" class="truck-grid"></div></div><div class="hitch-web"></div><div class="cab-web"><div class="cab-logo-web">CLIO</div></div></div></div></div><div class="glass panel-pad"><div class="d-flex justify-content-between align-items-center mb-2"><div class="fw-bold">Artículos cargados</div><div><button id="btnSave" class="btn btn-green-soft" type="button"><i class="bi bi-save me-1"></i>Guardar plan y artículos</button> <button id="btnPrint" class="btn btn-primary-soft d-none" type="button"><i class="bi bi-printer me-1"></i>Imprimir</button></div></div><div id="resultBox"></div><div class="table-wrap"><table class="table table-hover align-middle mb-0"><thead><tr><th>Artículo</th><th>Embalaje</th><th class="text-end">Tarimas</th><th class="text-end">Paquetes</th><th class="text-end">Piezas</th><th>OC</th><th></th></tr></thead><tbody id="loadBody"></tbody></table></div></div></div></div></div><div id="ocModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:1080;align-items:center;justify-content:center;padding:18px"><div class="bg-white rounded-4 shadow-lg" style="max-width:820px;width:100%;border:1px solid #dbe5f1"><div class="p-3 border-bottom d-flex justify-content-between align-items-center"><div><div class="fw-bold fs-5">Validar cliente y entrega de OC adicional</div><div class="small muted">La OC puede ser del mismo cliente u otro cliente. Confirma cliente y punto de entrega antes de cargar al camión.</div></div><button type="button" class="btn btn-outline-secondary btn-sm" id="ocModalClose"><i class="bi bi-x-lg"></i></button></div><div class="p-3"><div id="ocModalInfo" class="mb-3"></div><div class="row g-3"><div class="col-12"><label class="form-label fw-bold">Tipo de entrega para esta OC</label><div class="d-grid gap-2"><label class="border rounded-3 p-2"><input class="form-check-input me-2" type="radio" name="ocDeliveryMode" id="ocModeSame" value="same">Mismo cliente y misma dirección base</label><label class="border rounded-3 p-2"><input class="form-check-input me-2" type="radio" name="ocDeliveryMode" id="ocModeSameOther" value="same_other">Mismo cliente, otra dirección registrada</label><label class="border rounded-3 p-2"><input class="form-check-input me-2" type="radio" name="ocDeliveryMode" id="ocModeOther" value="other">Otro cliente</label></div></div><div class="col-12 col-md-6" id="ocCustomerBox"><label class="form-label fw-bold">Cliente para esta OC</label><select id="ocCustomerSelect" class="form-select"></select></div><div class="col-12 col-md-6"><label class="form-label fw-bold">Punto de entrega registrado</label><select id="ocAddressSelect" class="form-select"></select></div><div class="col-12"><label class="form-label fw-bold">Dirección seleccionada</label><textarea id="ocAddressText" class="form-control" rows="2" placeholder="Dirección de entrega"></textarea></div></div><div class="alert alert-warning small mt-3 mb-0"><b>Carga mixta:</b> si seleccionas otro cliente, el plan se imprimirá separado por cliente, OC y dirección. Almacén deberá validar documentos y ruta antes de surtir.</div></div><div class="p-3 border-top text-end"><button type="button" class="btn btn-outline-secondary" id="ocModalCancel">Cancelar</button> <button type="button" class="btn btn-primary" id="ocModalConfirm">Confirmar OC</button></div></div></div><div id="saveConfirmOverlay" style="display:none;position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:1090;align-items:center;justify-content:center;padding:18px"><div class="bg-white rounded-4 shadow-lg" style="max-width:920px;width:100%;border:1px solid #dbe5f1"><div class="p-3 border-bottom d-flex justify-content-between align-items-center"><div><div class="fw-bold fs-5"><i class="bi bi-check2-circle text-success me-1"></i> Confirmar plan de carga</div><div class="small muted">Revisa el resumen antes de guardar. Al confirmar, se actualizarán los artículos del pedido y el acomodo del camión.</div></div><button type="button" class="btn btn-outline-secondary btn-sm" id="saveConfirmClose"><i class="bi bi-x-lg"></i></button></div><div class="p-3"><div id="saveConfirmInfo" class="mb-3"></div><div class="table-wrap"><table class="table table-sm align-middle mb-0"><thead><tr><th>OC</th><th>Cliente</th><th>Dirección</th><th class="text-end">Tarimas</th></tr></thead><tbody id="saveConfirmBody"></tbody></table></div></div><div class="p-3 border-top text-end"><button type="button" class="btn btn-outline-secondary" id="saveConfirmCancel">Cancelar</button> <button type="button" class="btn btn-success fw-bold" id="saveConfirmOk"><i class="bi bi-save me-1"></i>Confirmar y guardar</button></div></div></div><div id="saveDetailOverlay" style="display:none;position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:1100;align-items:center;justify-content:center;padding:18px"><div class="bg-white rounded-4 shadow-lg" style="max-width:920px;width:100%;border:1px solid #dbe5f1"><div class="p-3 border-bottom d-flex justify-content-between align-items-center"><div><div class="fw-bold fs-5"><i class="bi bi-truck text-primary me-1"></i> Plan guardado correctamente</div><div class="small muted">Detalle final por OC, cliente y cantidad de tarimas.</div></div><button type="button" class="btn btn-outline-secondary btn-sm" id="saveDetailClose"><i class="bi bi-x-lg"></i></button></div><div class="p-3"><div id="saveDetailInfo" class="mb-3"></div><div class="table-wrap"><table class="table table-sm align-middle mb-0"><thead><tr><th>OC</th><th>Cliente</th><th>Dirección</th><th class="text-end">Tarimas</th></tr></thead><tbody id="saveDetailBody"></tbody></table></div></div><div class="p-3 border-top text-end"><button type="button" class="btn btn-primary-soft" id="saveDetailPrint"><i class="bi bi-printer me-1"></i>Imprimir</button> <button type="button" class="btn btn-success" id="saveDetailAccept">Aceptar</button></div></div></div><script>const CSRF=<?=json_encode($csrf)?>; const INITIAL_ORDER_ID=<?=json_encode($initialOrderId)?>; const $=id=>document.getElementById(id); let orders=[], products=[], currentOrder=null, packagingRows=[], loadRows=[], currentPlanId=0, pendingOcResolver=null, pendingOcContext=null; const h=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); const n=v=>Number(v||0); const i=v=>parseInt(String(v||0),10)||0; async function apiGet(a,p={}){const u=new URL(location.pathname,location.origin); u.searchParams.set('action',a); Object.entries(p).forEach(([k,v])=>u.searchParams.set(k,v)); const r=await fetch(u,{headers:{'X-CSRF':CSRF}}); return r.json();} async function apiPost(a,d={}){const u=new URL(location.pathname,location.origin); u.searchParams.set('action',a); const r=await fetch(u,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF':CSRF},body:JSON.stringify(d)}); return r.json();} function vehicleInfo(){return $('vehicleType').value==='TORTON'?{type:'TORTON',label:'Torton',capacity:18,cols:9}:{type:'CAJA_SECA',label:'Caja seca',capacity:30,cols:15};} async function bootstrap(){const j=await apiGet('bootstrap',{order_id:INITIAL_ORDER_ID||''}); if(!j.ok){alert(j.error||'Error');return;} orders=j.orders||[]; products=j.products||[]; $('orderId').innerHTML=(orders.length?'<option value="">Selecciona orden pendiente por surtir</option>':'<option value="">No hay pedidos pendientes por surtir</option>')+orders.map(o=>`<option value="${o.id}">${h(o.sales_order_ref||('OSV-'+o.id))} · ${h(o.business_name||o.contact_name||'')} · ${h(o.status||'')}</option>`).join(''); $('productId').innerHTML='<option value="">Selecciona producto</option>'+products.map(p=>`<option value="${p.id}">${h(p.sku)} · ${h(p.name)}</option>`).join(''); if(j.selected_order_id){$('orderId').value=String(j.selected_order_id); await loadOrder(j.selected_order_id);} await loadPackaging(); renderAll();} async function loadPackaging(){const pid=i($('productId').value); if(!pid){$('packagingId').innerHTML='<option value="">Selecciona artículo</option>'; packagingRows=[]; return;} const j=await apiGet('packaging_options',{product_id:pid}); packagingRows=j.rows||[]; $('packagingId').innerHTML=packagingRows.map(p=>`<option value="${p.id}">${h(p.packaging_name||p.packaging_type)} · ${i(p.packages_per_pallet)} paq/tar · ${i(p.pieces_per_package)} pz/paq</option>`).join(''); updateCalcInfo();} function updateCalcInfo(){const pk=packagingRows.find(x=>String(x.id)===$('packagingId').value)||packagingRows[0]; const pal=i($('pallets').value)||0; if(!pk){$('calcInfo').textContent='';return;} const packs=pal*i(pk.packages_per_pallet); const pieces=packs*i(pk.pieces_per_package); $('calcInfo').textContent=`${packs} paquetes · ${pieces} piezas`;}
async function loadOrder(id){if(!id){currentOrder=null;loadRows=[];currentPlanId=0;renderAll();return;} const j=await apiGet('order_detail',{order_id:id}); if(!j.ok){$('resultBox').innerHTML=`<div class="alert alert-danger">${h(j.error||'No se pudo cargar')}</div>`;return;} currentOrder=j.order; currentPlanId=n(j.plan?.id); $('vehicleType').value=j.plan?.vehicle_type||j.suggested_vehicle||'CAJA_SECA'; $('planNotes').value=j.plan?.notes||''; $('po').value=currentOrder.purchase_order||''; $('orderInfo').innerHTML=`<div class="order-active-box"><div class="fw-bold mono">${h(currentOrder.sales_order_ref||('OSV-'+currentOrder.id))}</div><div class="fw-bold">${h(currentOrder.business_name||currentOrder.contact_name||'')}</div><div class="small muted">OC base: <span class="mono">${h(currentOrder.purchase_order||'—')}</span> · Entrega: ${h(currentOrder.requested_delivery_date||'')} ${h(currentOrder.requested_delivery_time||'')}</div><div class="small muted">Destino base: ${h(currentOrder.delivery_address||'—')}</div></div>`;
 const items=j.items||[], slotRows=j.slots||[];
 const byItem={}; items.forEach(x=>{byItem[String(x.order_item_id||'')]=x;});
 if(slotRows.length){
   const groups={};
   slotRows.forEach(s=>{
     const key=[s.order_item_id||'',s.product_id||'',s.purchase_order||''].join('|');
     if(!groups[key]){
       const item=byItem[String(s.order_item_id||'')] || items.find(x=>String(x.product_id)===String(s.product_id)) || {};
       groups[key]={product_id:n(s.product_id||item.product_id),packaging_option_id:n(item.packaging_option_id),sku:item.sku||'',product_name:item.product_name||s.product_label||'',packaging_label:item.packaging_label||'',pallets_to_load:0,packages_per_pallet:i(item.packages_per_pallet)||i(s.packages_qty),pieces_per_package:i(item.pieces_per_package),packages_qty:0,pieces_qty:0,purchase_order:s.purchase_order||currentOrder.purchase_order||'',customer_id:n(s.customer_id||currentOrder.customer_id),address_id:n(s.address_id||currentOrder.address_id),customer_label:s.customer_label||currentOrder.business_name||currentOrder.contact_name||'',delivery_address:s.delivery_address||currentOrder.delivery_address||'',pallet_type:item.pallet_type||'Chep',pallet_specification:item.pallet_specification||'retorno'};
     }
     groups[key].pallets_to_load++;
     groups[key].packages_qty += i(s.packages_qty);
     groups[key].pieces_qty += i(s.pieces_qty);
   });
   loadRows=Object.values(groups);
 } else {
   loadRows=items.map(x=>({product_id:n(x.product_id),packaging_option_id:n(x.packaging_option_id),sku:x.sku,product_name:x.product_name,packaging_label:x.packaging_label,pallets_to_load:i(x.pallets_qty),packages_per_pallet:i(x.packages_per_pallet),pieces_per_package:i(x.pieces_per_package),packages_qty:i(x.packages_qty),pieces_qty:i(x.qty),purchase_order:currentOrder.purchase_order||'',customer_id:n(currentOrder.customer_id),address_id:n(currentOrder.address_id),customer_label:currentOrder.business_name||currentOrder.contact_name||'',delivery_address:currentOrder.delivery_address||'',pallet_type:x.pallet_type||'Chep',pallet_specification:x.pallet_specification||'retorno'}));
 }
 $('btnPrint').classList.toggle('d-none',!currentPlanId); renderAll();}
function normalizePo(v){return String(v||'').trim().toUpperCase();}
function needsOcValidation(po){
  const base=normalizePo(currentOrder?.purchase_order||'');
  const val=normalizePo(po);
  if(!val) return false;
  if(val!==base) return true;
  return loadRows.some(r=>normalizePo(r.purchase_order)!==val);
}
async function openOcModal(po){
  const j=await apiGet('oc_context',{order_id:n(currentOrder?.id),purchase_order:po});
  if(!j.ok){$('resultBox').innerHTML=`<div class="alert alert-danger">${h(j.error||'No se pudo validar la OC')}</div>`;return null;}
  pendingOcContext=j;
  const oc=j.oc_order||{}, base=j.base_order||{};
  const same=!!n(oc.same_customer);
  $('ocModalInfo').innerHTML=`<div class="alert ${j.matched?'alert-info':'alert-warning'} mb-2"><div class="fw-bold">OC ${h(po||'SIN OC')}</div><div>${j.matched?'Se encontró una orden relacionada para esta OC.':'No se encontró otra orden con esta OC; selecciona cliente y dirección.'}</div></div><div class="row g-2"><div class="col-md-6"><div class="small muted">Cliente de la orden base</div><div class="fw-bold">${h(base.customer_label||'')}</div><div class="small muted">${h(base.delivery_address||'')}</div></div><div class="col-md-6"><div class="small muted">Cliente sugerido por OC</div><div class="fw-bold">${h(oc.customer_label||'Sin coincidencia')}</div><div class="small muted">${h(oc.delivery_address||'')}</div></div></div>`;

  const customers=j.customers||[];
  $('ocCustomerSelect').innerHTML='<option value="">Selecciona cliente</option>'+customers.map(c=>{
    const label=[c.customer_code, c.business_name||c.contact_name].filter(Boolean).join(' · ');
    return `<option value="${c.id}" data-label="${h(c.business_name||c.contact_name||'')}">${h(label)}</option>`;
  }).join('');

  if(same){ $('ocModeSame').checked=true; }
  else if(n(oc.customer_id) && n(oc.customer_id)!==n(base.customer_id)){ $('ocModeOther').checked=true; }
  else { $('ocModeOther').checked=true; }

  const selectedCustomerId = n(oc.customer_id||base.customer_id);
  $('ocCustomerSelect').value = selectedCustomerId ? String(selectedCustomerId) : '';
  await loadOcAddresses(selectedCustomerId, j.addresses||[]);

  if($('ocModeSame').checked){
    $('ocAddressText').value = base.delivery_address||'';
    $('ocAddressSelect').value='';
  } else if($('ocModeSameOther').checked){
    $('ocAddressText').value = '';
  } else {
    $('ocAddressText').value = oc.delivery_address||'';
  }
  refreshOcModalMode();
  $('ocModalOverlay').style.display='flex';
  return await new Promise(resolve=>{ pendingOcResolver=resolve; });
}
async function loadOcAddresses(customerId, presetRows=null){
  let rows=Array.isArray(presetRows)?presetRows:null;
  if(rows===null){
    const j=await apiGet('customer_addresses',{customer_id:customerId||0});
    rows=(j&&j.ok)?(j.rows||[]):[];
  }
  $('ocAddressSelect').innerHTML='<option value="">Selecciona dirección registrada</option>'+rows.map(a=>`<option value="${a.id}" data-address="${h(a.address||'')}">${h(a.address||'')}</option>`).join('');
}
function ocMode(){return document.querySelector('input[name="ocDeliveryMode"]:checked')?.value || 'other';}
function refreshOcModalMode(){
  const mode=ocMode(), ctx=pendingOcContext||{}, base=ctx.base_order||{};
  const isOther=mode==='other';
  $('ocCustomerBox').style.display=isOther?'block':'none';
  $('ocCustomerSelect').disabled=!isOther;
  if(mode==='same'){
    $('ocAddressSelect').disabled=true;
    $('ocAddressSelect').value='';
    $('ocAddressText').value=base.delivery_address||'';
  } else {
    $('ocAddressSelect').disabled=false;
    if(mode==='same_other'){
      $('ocCustomerSelect').value=String(base.customer_id||'');
      loadOcAddresses(n(base.customer_id));
    }
  }
}
function closeOcModal(result){
  $('ocModalOverlay').style.display='none';
  const resolve=pendingOcResolver; pendingOcResolver=null;
  if(resolve) resolve(result||null);
}
function selectedOcMeta(){
  const ctx=pendingOcContext||{}; const oc=ctx.oc_order||{}; const base=ctx.base_order||{}; const mode=ocMode();
  let customerId=n(base.customer_id), customerLabel=base.customer_label||'', addressId=n(base.address_id), address=base.delivery_address||'';
  if(mode==='same'){
    customerId=n(base.customer_id); customerLabel=base.customer_label||''; addressId=n(base.address_id); address=base.delivery_address||'';
  } else if(mode==='same_other'){
    customerId=n(base.customer_id); customerLabel=base.customer_label||''; addressId=0; address='';
  } else {
    customerId=n($('ocCustomerSelect').value||oc.customer_id||base.customer_id);
    const optC=$('ocCustomerSelect').selectedOptions[0];
    customerLabel=(optC && optC.value) ? (optC.getAttribute('data-label')||optC.textContent||'') : (oc.customer_label||base.customer_label||'');
    addressId=n(oc.address_id||0); address=oc.delivery_address||'';
  }
  const opt=$('ocAddressSelect').selectedOptions[0];
  if(opt && opt.value){ addressId=n(opt.value); address=opt.getAttribute('data-address')||opt.textContent||address; }
  const manual=$('ocAddressText').value.trim();
  if(manual!=='') address=manual;
  if(!customerId){ $('resultBox').innerHTML='<div class="alert alert-warning">Selecciona el cliente de la OC adicional.</div>'; return null; }
  if(!String(address||'').trim()){ $('resultBox').innerHTML='<div class="alert alert-warning">Selecciona o captura la dirección de entrega de la OC adicional.</div>'; return null; }
  return {customer_id:customerId,address_id:addressId,customer_label:customerLabel,delivery_address:address};
}
async function addItem(){
  const pid=i($('productId').value);
  const pk=packagingRows.find(x=>String(x.id)===$('packagingId').value)||packagingRows[0];
  const pal=i($('pallets').value);
  if(!currentOrder){$('resultBox').innerHTML='<div class="alert alert-warning">Selecciona una orden.</div>';return;}
  if(!pid||!pk||pal<=0){$('resultBox').innerHTML='<div class="alert alert-warning">Selecciona artículo, embalaje y tarimas.</div>';return;}
  const poValue=$('po').value||currentOrder.purchase_order||'';
  let ocMeta={customer_id:n(currentOrder.customer_id),address_id:n(currentOrder.address_id),customer_label:currentOrder.business_name||currentOrder.contact_name||'',delivery_address:currentOrder.delivery_address||''};
  if(needsOcValidation(poValue)){
    const confirmed=await openOcModal(poValue);
    if(!confirmed) return;
    ocMeta=confirmed;
  }
  const pr=products.find(p=>n(p.id)===pid)||{};
  const packs=pal*i(pk.packages_per_pallet);
  const pieces=packs*i(pk.pieces_per_package);
  loadRows.push({product_id:pid,packaging_option_id:i(pk.id),sku:pr.sku||'',product_name:pr.name||'',packaging_label:pk.packaging_name||pk.packaging_type||'',pallets_to_load:pal,packages_per_pallet:i(pk.packages_per_pallet),pieces_per_package:i(pk.pieces_per_package),packages_qty:packs,pieces_qty:pieces,purchase_order:poValue,customer_id:ocMeta.customer_id,address_id:ocMeta.address_id,customer_label:ocMeta.customer_label,delivery_address:ocMeta.delivery_address,pallet_type:$('palletType').value,pallet_specification:$('palletSpec').value});
  renderAll();
}
function removeRow(idx){loadRows.splice(idx,1); renderAll();}
function totalPallets(){return loadRows.reduce((acc,r)=>acc+i(r.pallets_to_load),0);}
function slots(){
  const out=[];
  const v=vehicleInfo();
  let slot=v.capacity;
  for(const r of loadRows){
    for(let k=0;k<i(r.pallets_to_load);k++){
      out.push({slot_no:slot--, product_label:`${r.sku} ${r.product_name}`, purchase_order:r.purchase_order, customer_label:r.customer_label, delivery_address:r.delivery_address});
    }
  }
  return out;
}
function displaySlotOrder(capacity){const arr=[]; for(let k=1;k<=capacity;k+=2) arr.push(k); for(let k=2;k<=capacity;k+=2) arr.push(k); return arr;}
function ocKey(x){const v=String(x?.purchase_order||'SIN OC').trim().toUpperCase(); return v||'SIN OC';}
function ocColorMap(rows){const map={}; let idx=0; rows.forEach(x=>{const k=ocKey(x); if(!map[k]) map[k]=`oc-color-${idx++%10}`;}); return map;}
function loadMessage(type='live'){
  const v=vehicleInfo();
  const used=totalPallets();
  const free=Math.max(0,v.capacity-used);
  if(used<=0) return '';
  if(used>30){return `<div class="alert alert-danger">La carga excede la capacidad máxima disponible. Caja seca permite 30 tarimas y Torton 18. Capturado: <b>${used}</b> tarimas.</div>`;}
  if(used>v.capacity){
    const suggestion = v.type==='TORTON' && used<=30 ? ' Cambia la unidad a Caja seca para permitir hasta 30 tarimas.' : '';
    return `<div class="alert alert-danger">La carga excede la capacidad de ${h(v.label)}: ${used}/${v.capacity} tarimas.${suggestion}</div>`;
  }
  if(v.type==='CAJA_SECA' && used<=16){
    return `<div class="alert alert-warning">Estás cargando ${used} tarima(s) en Caja seca. La unidad queda muy grande; se sugiere usar Torton cuando la carga sea de 16 tarimas o menos.</div>`;
  }
  if(free>0){return `<div class="alert alert-info">La unidad aún tiene <b>${free}</b> espacio(s) disponible(s) de ${v.capacity}. Puedes agregar otra OC o guardar el plan así.</div>`;}
  return `<div class="alert alert-success">La unidad está completa: ${used}/${v.capacity} espacios ocupados.</div>`;
}
function hasBlockingCapacityError(){const used=totalPallets(); const v=vehicleInfo(); return used>v.capacity || used>30;}
function renderAll(){
  const v=vehicleInfo(), s=slots(), colors=ocColorMap(s);
  $('kVehicle').textContent=v.label;
  $('kCapacity').textContent=v.capacity;
  $('kUsed').textContent=s.length;
  $('kFree').textContent=Math.max(0,v.capacity-s.length);
  $('trailerHead').textContent=`${v.label} · ${v.capacity} espacios · acomodo 2 filas · interior → exterior`;
  $('truckGrid').style.gridTemplateColumns=`repeat(${v.cols},80px)`;
  const map={}; s.forEach(x=>map[x.slot_no]=x);
  $('truckGrid').innerHTML=displaySlotOrder(v.capacity).map(no=>{
    const x=map[no];
    const cls=x ? colors[ocKey(x)] : 'empty';
    return `<div class="slot ${cls}"><div class="slot-no">${no}</div><div class="oc">${h(x?.purchase_order||'')}</div><div class="prod">${h((x?.product_label||'').slice(0,18))}</div></div>`;
  }).join('');
  $('loadBody').innerHTML=loadRows.length?loadRows.map((r,idx)=>{
    const cls=ocColorMap(loadRows.map(row=>({purchase_order:row.purchase_order})))[ocKey({purchase_order:r.purchase_order})]||'oc-color-0';
    return `<tr><td><div class="fw-bold mono">${h(r.sku)}</div><div class="small muted">${h(r.product_name)}</div></td><td>${h(r.packaging_label)}</td><td class="text-end mono fw-bold">${i(r.pallets_to_load)}</td><td class="text-end mono">${i(r.packages_qty)}</td><td class="text-end mono">${i(r.pieces_qty)}</td><td class="mono"><span class="slot ${cls}" style="display:inline-flex;width:auto;height:auto;min-width:78px;padding:4px 8px;border-radius:8px">${h(r.purchase_order)}</span><div class="small muted mt-1">${h(r.customer_label||'')}</div><div class="small muted">${h((r.delivery_address||'').slice(0,55))}</div></td><td class="text-end"><button class="btn btn-outline-danger btn-sm" onclick="removeRow(${idx})"><i class="bi bi-x"></i></button></td></tr>`;
  }).join(''):'<tr><td colspan="7" class="text-center muted py-4">Agrega artículos para acomodarlos en el camión. Puedes agregar más de una OC cambiando el campo OC antes de agregar cada bloque.</td></tr>';
  $('resultBox').innerHTML=loadMessage('live');
  $('btnSave').disabled=hasBlockingCapacityError()||!currentOrder||!loadRows.length;
}
function groupLoadByOcCustomer(){
  const map={};
  for(const r of loadRows){
    const oc=String(r.purchase_order||'SIN OC').trim()||'SIN OC';
    const customer=String(r.customer_label||currentOrder?.business_name||currentOrder?.contact_name||'Cliente sin nombre').trim();
    const address=String(r.delivery_address||currentOrder?.delivery_address||'Sin dirección').trim();
    const key=[oc,customer,address].join('|');
    if(!map[key]) map[key]={purchase_order:oc,customer_label:customer,delivery_address:address,pallets:0};
    map[key].pallets += i(r.pallets_to_load);
  }
  return Object.values(map);
}
function summaryRowsHtml(rows){
  if(!rows.length) return '<tr><td colspan="4" class="text-center muted py-3">Sin carga capturada.</td></tr>';
  return rows.map(r=>`<tr><td class="mono fw-bold">${h(r.purchase_order)}</td><td>${h(r.customer_label)}</td><td class="small muted">${h(r.delivery_address)}</td><td class="text-end mono fw-bold">${i(r.pallets)}</td></tr>`).join('');
}
function openSaveConfirmModal(){
  const rows=groupLoadByOcCustomer();
  const v=vehicleInfo();
  const used=totalPallets();
  const free=Math.max(0,v.capacity-used);
  $('saveConfirmInfo').innerHTML=`<div class="alert alert-light border mb-0"><b>Unidad:</b> ${h(v.label)} · <b>Tarimas:</b> ${used}/${v.capacity} · <b>Disponibles:</b> ${free}</div>${loadMessage('save')}`;
  $('saveConfirmBody').innerHTML=summaryRowsHtml(rows);
  $('saveConfirmOverlay').style.display='flex';
}
function closeSaveConfirmModal(){ $('saveConfirmOverlay').style.display='none'; }
function openSaveDetailModal(apiResponse){
  const rows=groupLoadByOcCustomer();
  const free=n(apiResponse?.free_spaces||0);
  const vehicle=h(apiResponse?.vehicle_label||vehicleInfo().label);
  $('saveDetailInfo').innerHTML= free>0 ? `<div class="alert alert-info mb-0">Plan guardado en ${vehicle}. Quedan <b>${free}</b> espacio(s) disponible(s).</div>` : `<div class="alert alert-success mb-0">Plan guardado en ${vehicle}. La unidad quedó completa.</div>`;
  $('saveDetailBody').innerHTML=summaryRowsHtml(rows);
  $('saveDetailOverlay').style.display='flex';
}
function closeSaveDetailModal(){ $('saveDetailOverlay').style.display='none'; }
async function savePlan(){
  if(!currentOrder)return;
  if(hasBlockingCapacityError()){ $('resultBox').innerHTML=loadMessage('save'); return; }
  openSaveConfirmModal();
}
async function executeSavePlan(){
  if(!currentOrder)return;
  if(hasBlockingCapacityError()){ closeSaveConfirmModal(); $('resultBox').innerHTML=loadMessage('save'); return; }
  const v=vehicleInfo();
  const beforeMessage=loadMessage('save');
  closeSaveConfirmModal();
  $('btnSave').disabled=true;
  $('btnSave').innerHTML='<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
  const j=await apiPost('save_plan',{order_id:n(currentOrder.id),vehicle_type:v.type,notes:$('planNotes').value,items:loadRows});
  $('btnSave').disabled=false;
  $('btnSave').innerHTML='<i class="bi bi-save me-1"></i>Guardar plan y artículos';
  if(!j.ok){$('resultBox').innerHTML=`<div class="alert alert-danger">${h(j.error||'No se pudo guardar')}</div>`;return;}
  currentPlanId=n(j.plan_id);
  $('btnPrint').classList.remove('d-none');
  const freeMsg = n(j.free_spaces)>0 ? `<div class="alert alert-info mt-2">Plan guardado. Aún quedan <b>${h(j.free_spaces)}</b> espacio(s) disponible(s) en ${h(j.vehicle_label)}.</div>` : `<div class="alert alert-success mt-2">Plan guardado. La unidad quedó completa.</div>`;
  $('resultBox').innerHTML=`<div class="alert alert-success">Plan guardado: ${h(j.slots_count)} tarimas en ${h(j.vehicle_label)}. Almacén podrá ver esta carga.</div>${beforeMessage}${freeMsg}`;
  openSaveDetailModal(j);
  await loadOrder(currentOrder.id);
}
function printPlan(){if(currentPlanId>0) window.open(`carga_camion_botella.php?action=print&plan_id=${encodeURIComponent(currentPlanId)}`,'_blank','noopener');}
function bindClick(id, fn){
  const el=$(id);
  if(el) el.addEventListener('click', fn);
}
bindClick('ocModalClose',()=>closeOcModal(null));
bindClick('ocModalCancel',()=>closeOcModal(null));
bindClick('ocModalConfirm',()=>{const meta=selectedOcMeta(); if(meta) closeOcModal(meta);});
bindClick('saveConfirmClose', closeSaveConfirmModal);
bindClick('saveConfirmCancel', closeSaveConfirmModal);
bindClick('saveConfirmOk', executeSavePlan);
bindClick('saveDetailClose', closeSaveDetailModal);
bindClick('saveDetailAccept', closeSaveDetailModal);
bindClick('saveDetailPrint', printPlan); ['ocModeSame','ocModeSameOther','ocModeOther'].forEach(id=>$(id).addEventListener('change',refreshOcModalMode)); $('ocCustomerSelect').addEventListener('change',async e=>{await loadOcAddresses(n(e.target.value)); $('ocAddressText').value='';}); $('ocAddressSelect').addEventListener('change',e=>{const opt=e.target.selectedOptions[0]; if(opt&&opt.value){$('ocAddressText').value=opt.getAttribute('data-address')||opt.textContent||'';}}); $('orderId').addEventListener('change',e=>loadOrder(e.target.value)); $('productId').addEventListener('change',loadPackaging); $('packagingId').addEventListener('change',updateCalcInfo); $('pallets').addEventListener('input',updateCalcInfo); $('btnAdd').addEventListener('click',addItem); $('btnClear').addEventListener('click',()=>{loadRows=[];renderAll();}); $('vehicleType').addEventListener('change',renderAll); $('btnSave').addEventListener('click',savePlan); $('btnPrint').addEventListener('click',printPlan); bootstrap();</script></body></html>
