<?php
declare(strict_types=1);
/**
 * BReport · Montacargas Botella
 * ------------------------------------------------------------
 * El montacarguista consulta órdenes enviadas por Almacén y confirma
 * carga completa del camión. No afecta inventario.
 */
const BREPORT_TIMEZONE = 'America/Matamoros';
date_default_timezone_set(BREPORT_TIMEZONE);

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

require_scope();
ensure_session();

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

function fl_h(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function fl_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 fl_read_json(): array { $raw=file_get_contents('php://input'); $d=json_decode($raw?:'[]', true); return is_array($d)?$d:[]; }
function fl_need_csrf(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') return; $hdr=(string)($_SERVER['HTTP_X_CSRF'] ?? ''); if($hdr==='') fl_jexit(['ok'=>false,'error'=>'Falta encabezado X-CSRF'],419); csrf_verify($hdr); }
function fl_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 fl_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 fl_add_col(PDO $pdo, string $table, string $col, string $def): void { if(!fl_col_exists($pdo,$table,$col)){ try{$pdo->exec("ALTER TABLE `$table` ADD COLUMN $def");}catch(Throwable $e){} } }
function fl_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 fl_can_access(): bool { return has_role('admin') || has_role('warehouse') || has_role('almacen') || has_role('supervisor') || has_role('corp') || has_role('plant_admin') || has_role('montacargas') || has_role('forklift'); }

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

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

fl_ensure_schema($pdo);
if(!fl_scope_is_botella($pdo,$companyId,$locationId)){ http_response_code(403); exit('Esta sección corresponde únicamente a Botella.'); }
if(!fl_can_access()){ http_response_code(403); exit('Acceso denegado.'); }

function fl_orders(PDO $pdo, int $companyId, int $locationId, string $status='', string $date='', int $page=1, int $perPage=20): array {
  $page=max(1,$page);
  $perPage=max(5,min(50,$perPage));
  if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)) {
    $date=(new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE)))->format('Y-m-d');
  }

  $where=["fo.company_id=?","fo.location_id=?"];
  $params=[$companyId,$locationId];

  if($status!=='') {
    $where[]="UPPER(fo.status)=?";
    $params[]=strtoupper($status);
  } else {
    $where[]="UPPER(fo.status) IN ('PENDIENTE','EN_PROCESO','COMPLETA')";
  }

  /*
   * Filtro operativo por fecha:
   * Se toma primero la fecha solicitada de entrega del pedido y, como respaldo,
   * la fecha de creación de la orden de montacargas. Así no se mezclan órdenes
   * de otros días y se conserva rastreabilidad real contra BD.
   */
  $where[]="(DATE(COALESCE(so.requested_delivery_date, fo.created_at)) = ? OR DATE(fo.created_at) = ?)";
  $params[]=$date;
  $params[]=$date;

  $whereSql=implode(' AND ',$where);

  $stCount=$pdo->prepare("SELECT COUNT(DISTINCT fo.id)
    FROM warehouse_forklift_load_orders fo
    JOIN sales_orders so ON so.id=fo.order_id
    WHERE {$whereSql}");
  $stCount->execute($params);
  $total=(int)$stCount->fetchColumn();
  $pages=max(1,(int)ceil($total/$perPage));
  if($page>$pages) $page=$pages;
  $offset=($page-1)*$perPage;

  $sql="SELECT fo.*, so.sales_order_ref, so.purchase_order, so.requested_delivery_date, so.requested_delivery_time, so.route_no, so.delivery_address, c.business_name, c.contact_name,
      ustart.full_name AS load_started_by_name,
      uconf.full_name AS confirmed_by_name,
      ucreate.full_name AS created_by_name,
      COUNT(fi.id) AS total_slots,
      COALESCE(SUM(fi.pallets_qty),0) AS total_pallets,
      COALESCE(SUM(fi.loaded),0) AS loaded_slots
    FROM warehouse_forklift_load_orders fo
    JOIN sales_orders so ON so.id=fo.order_id
    JOIN sales_customers c ON c.id=so.customer_id
    LEFT JOIN users ustart ON ustart.id=fo.load_started_by
    LEFT JOIN users uconf ON uconf.id=fo.confirmed_by
    LEFT JOIN users ucreate ON ucreate.id=fo.created_by
    LEFT JOIN warehouse_forklift_load_order_items fi ON fi.forklift_order_id=fo.id
    WHERE {$whereSql}
    GROUP BY fo.id, so.sales_order_ref, so.purchase_order, so.requested_delivery_date, so.requested_delivery_time, so.route_no, so.delivery_address, c.business_name, c.contact_name, ustart.full_name, uconf.full_name, ucreate.full_name
    ORDER BY FIELD(UPPER(fo.status),'PENDIENTE','EN_PROCESO','COMPLETA'), fo.id DESC
    LIMIT {$perPage} OFFSET {$offset}";
  $st=$pdo->prepare($sql);
  $st->execute($params);
  return [
    'rows'=>$st->fetchAll(PDO::FETCH_ASSOC),
    'meta'=>[
      'date'=>$date,
      'page'=>$page,
      'per_page'=>$perPage,
      'total'=>$total,
      'pages'=>$pages,
    ]
  ];
}
function fl_detail(PDO $pdo, int $companyId, int $locationId, int $id): array {
  $st=$pdo->prepare("SELECT fo.*, so.sales_order_ref, so.purchase_order, so.requested_delivery_date, so.requested_delivery_time, so.route_no, so.delivery_address, c.business_name, c.contact_name,
      ustart.full_name AS load_started_by_name,
      uconf.full_name AS confirmed_by_name,
      ucreate.full_name AS created_by_name
    FROM warehouse_forklift_load_orders fo
    JOIN sales_orders so ON so.id=fo.order_id
    JOIN sales_customers c ON c.id=so.customer_id
    LEFT JOIN users ustart ON ustart.id=fo.load_started_by
    LEFT JOIN users uconf ON uconf.id=fo.confirmed_by
    LEFT JOIN users ucreate ON ucreate.id=fo.created_by
    WHERE fo.id=? AND fo.company_id=? AND fo.location_id=? LIMIT 1");
  $st->execute([$id,$companyId,$locationId]); $order=$st->fetch(PDO::FETCH_ASSOC);
  if(!$order) throw new RuntimeException('Orden no encontrada.');
  $st=$pdo->prepare("SELECT fi.*, u.full_name AS loaded_by_name
    FROM warehouse_forklift_load_order_items fi
    LEFT JOIN users u ON u.id=fi.loaded_by
    WHERE fi.forklift_order_id=?
    ORDER BY fi.slot_no ASC");
  $st->execute([$id]); $items=$st->fetchAll(PDO::FETCH_ASSOC);
  return ['order'=>$order,'items'=>$items];
}


function fl_fmt_duration(int $seconds): string {
  $seconds=max(0,$seconds);
  $h=intdiv($seconds,3600);
  $m=intdiv($seconds%3600,60);
  $s=$seconds%60;
  if($h>0) return sprintf('%dh %02dm %02ds',$h,$m,$s);
  if($m>0) return sprintf('%dm %02ds',$m,$s);
  return sprintf('%ds',$s);
}

function fl_parse_product_id_from_label(string $labelCode): int {
  $labelCode = strtoupper(trim($labelCode));
  if ($labelCode === '') return 0;
  if (preg_match('/^A(\d+)(?:[-_:\|]|$)/', $labelCode, $m)) return (int)$m[1];
  if (preg_match('/(?:^|[^A-Z0-9])A(\d+)(?:[^0-9]|$)/', $labelCode, $m)) return (int)$m[1];
  return 0;
}

function fl_label_product(PDO $pdo, int $companyId, int $locationId, string $labelCode): array {
  $labelCode = trim($labelCode);
  if ($labelCode === '') throw new RuntimeException('Escanea o captura el código de la tarima.');

  if (fl_table_exists($pdo, 'production_labels')) {
    $st = $pdo->prepare("SELECT id, label_code, product_id FROM production_labels WHERE company_id=? AND location_id=? AND label_code=? LIMIT 1");
    $st->execute([$companyId, $locationId, $labelCode]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if ($row) {
      return [
        'label_id' => (int)$row['id'],
        'label_code' => (string)$row['label_code'],
        'product_id' => (int)$row['product_id'],
        'source' => 'production_labels',
      ];
    }
  }

  $productId = fl_parse_product_id_from_label($labelCode);
  if ($productId <= 0) {
    throw new RuntimeException('Código no reconocido. Formato esperado: A{id_articulo}-fecha-hora-consecutivo. Ejemplo: A18-20260617-143010-0001.');
  }
  return [
    'label_id' => null,
    'label_code' => $labelCode,
    'product_id' => $productId,
    'source' => 'parsed_code',
  ];
}

function fl_current_group_from_items(array $items): ?array {
  $groups = [];
  foreach ($items as $it) {
    $oc = trim((string)($it['purchase_order'] ?? 'SIN OC')) ?: 'SIN OC';
    $pid = (int)($it['product_id'] ?? 0);
    $label = trim((string)($it['product_label'] ?? 'SIN ARTÍCULO')) ?: 'SIN ARTÍCULO';
    $key = $oc . '||' . $pid . '||' . $label;
    if (!isset($groups[$key])) {
      $groups[$key] = [
        'key'=>$key,
        'purchase_order'=>$oc,
        'product_id'=>$pid,
        'product_label'=>$label,
        'total'=>0,
        'loaded'=>0,
        'min_slot'=>999999,
      ];
    }
    $groups[$key]['total']++;
    if ((int)($it['loaded'] ?? 0) === 1) $groups[$key]['loaded']++;
    $groups[$key]['min_slot'] = min($groups[$key]['min_slot'], (int)($it['slot_no'] ?? 0));
  }
  usort($groups, fn($a,$b) => ((int)$a['min_slot'] <=> (int)$b['min_slot']));
  foreach ($groups as $g) {
    if ((int)$g['loaded'] < (int)$g['total']) return $g;
  }
  return null;
}


function fl_loaded_pieces_check(PDO $pdo, int $forkliftOrderId): array {
  $st = $pdo->prepare("SELECT
      fi.id,
      fi.slot_no,
      fi.purchase_order,
      fi.product_label,
      fi.loaded,
      fi.scan_mode,
      fi.pieces_qty AS expected_pieces,
      fi.scanned_label_id,
      fi.scanned_label_code,
      COALESCE(pl.total_pieces, CASE WHEN fi.loaded=1 THEN fi.pieces_qty ELSE 0 END) AS scanned_pieces
    FROM warehouse_forklift_load_order_items fi
    LEFT JOIN production_labels pl ON pl.id = fi.scanned_label_id
    WHERE fi.forklift_order_id=?
    ORDER BY fi.slot_no ASC");
  $st->execute([$forkliftOrderId]);
  $items = $st->fetchAll(PDO::FETCH_ASSOC);

  $expectedTotal = 0.0;
  $scannedTotal = 0.0;
  $rows = [];
  foreach ($items as $it) {
    $expected = (float)($it['expected_pieces'] ?? 0);
    $scanned = ((int)($it['loaded'] ?? 0) === 1) ? (float)($it['scanned_pieces'] ?? 0) : 0.0;
    $missing = max(0.0, $expected - $scanned);
    $expectedTotal += $expected;
    $scannedTotal += $scanned;
    $rows[] = [
      'item_id'=>(int)($it['id'] ?? 0),
      'slot_no'=>(int)($it['slot_no'] ?? 0),
      'purchase_order'=>(string)($it['purchase_order'] ?? 'SIN OC'),
      'product_label'=>(string)($it['product_label'] ?? ''),
      'loaded'=>(int)($it['loaded'] ?? 0),
      'scan_mode'=>(string)($it['scan_mode'] ?? ''),
      'label_code'=>(string)($it['scanned_label_code'] ?? ''),
      'expected_pieces'=>$expected,
      'scanned_pieces'=>$scanned,
      'missing_pieces'=>$missing,
    ];
  }
  $missingTotal = max(0.0, $expectedTotal - $scannedTotal);
  return [
    'ok'=>$missingTotal <= 0.0001,
    'expected_pieces'=>$expectedTotal,
    'scanned_pieces'=>$scannedTotal,
    'missing_pieces'=>$missingTotal,
    'rows'=>$rows,
    'missing_rows'=>array_values(array_filter($rows, fn($r) => (float)$r['missing_pieces'] > 0.0001)),
  ];
}

function fl_release_scanned_label(PDO $pdo, int $companyId, int $locationId, int $userId, int $forkliftOrderId, string $labelCode, int $itemId=0): array {
  $labelCode = trim($labelCode);
  if ($labelCode === '' && $itemId <= 0) throw new RuntimeException('Indica la etiqueta o el registro a liberar.');

  $pdo->beginTransaction();
  try {
    if ($itemId > 0) {
      $st = $pdo->prepare("SELECT fi.*, fo.status AS order_status, fo.id AS forklift_order_id
        FROM warehouse_forklift_load_order_items fi
        JOIN warehouse_forklift_load_orders fo ON fo.id=fi.forklift_order_id
        WHERE fi.id=? AND fo.company_id=? AND fo.location_id=? FOR UPDATE");
      $st->execute([$itemId, $companyId, $locationId]);
    } else {
      $st = $pdo->prepare("SELECT fi.*, fo.status AS order_status, fo.id AS forklift_order_id
        FROM warehouse_forklift_load_order_items fi
        JOIN warehouse_forklift_load_orders fo ON fo.id=fi.forklift_order_id
        WHERE fo.company_id=? AND fo.location_id=? AND fi.loaded=1 AND fi.scanned_label_code=?
        ORDER BY fi.scanned_at DESC, fi.id DESC LIMIT 1 FOR UPDATE");
      $st->execute([$companyId, $locationId, $labelCode]);
    }
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) throw new RuntimeException('No se encontró una lectura activa para liberar.');
    if (strtoupper((string)($row['order_status'] ?? '')) === 'COMPLETA') {
      throw new RuntimeException('La orden ya está completa; no se puede liberar la etiqueta desde montacargas.');
    }
    if ((int)$forkliftOrderId > 0 && (int)$row['forklift_order_id'] !== $forkliftOrderId) {
      // Se permite liberar solo si la orden donde está tomada no está completa, pero avisamos con claridad.
    }

    $releasedCode = (string)($row['scanned_label_code'] ?? $labelCode);
    $st = $pdo->prepare("UPDATE warehouse_forklift_load_order_items
      SET loaded=0,
          loaded_at=NULL,
          loaded_by=NULL,
          scan_mode=NULL,
          scanned_label_id=NULL,
          scanned_label_code=NULL,
          scanned_at=NULL
      WHERE id=?");
    $st->execute([(int)$row['id']]);

    $pdo->commit();
    return [
      'item_id'=>(int)$row['id'],
      'forklift_order_id'=>(int)$row['forklift_order_id'],
      'slot_no'=>(int)($row['slot_no'] ?? 0),
      'purchase_order'=>(string)($row['purchase_order'] ?? ''),
      'product_label'=>(string)($row['product_label'] ?? ''),
      'label_code'=>$releasedCode,
    ];
  } catch (Throwable $e) {
    if ($pdo->inTransaction()) $pdo->rollBack();
    throw $e;
  }
}

function fl_scan_label(PDO $pdo, int $companyId, int $locationId, int $userId, int $forkliftOrderId, string $labelCode): array {
  $label = fl_label_product($pdo, $companyId, $locationId, $labelCode);
  $productId = (int)$label['product_id'];
  if ($productId <= 0) throw new RuntimeException('El código escaneado no contiene un artículo válido.');

  $pdo->beginTransaction();
  try {
    $st = $pdo->prepare("SELECT * FROM warehouse_forklift_load_orders WHERE id=? AND company_id=? AND location_id=? FOR UPDATE");
    $st->execute([$forkliftOrderId, $companyId, $locationId]);
    $order = $st->fetch(PDO::FETCH_ASSOC);
    if (!$order) throw new RuntimeException('Orden de montacargas no encontrada.');
    if (strtoupper((string)$order['status']) === 'COMPLETA') throw new RuntimeException('La orden ya está completa; no permite más lecturas.');

    $st = $pdo->prepare("SELECT * FROM warehouse_forklift_load_order_items WHERE forklift_order_id=? ORDER BY slot_no ASC FOR UPDATE");
    $st->execute([$forkliftOrderId]);
    $items = $st->fetchAll(PDO::FETCH_ASSOC);
    if (!$items) throw new RuntimeException('La orden no tiene tarimas configuradas.');

    $current = fl_current_group_from_items($items);
    if (!$current) throw new RuntimeException('Todas las tarimas de esta orden ya están cargadas.');

    if ((int)$current['product_id'] !== $productId) {
      $expected = (string)$current['product_label'];
      throw new RuntimeException('ARTÍCULO INCORRECTO: la lectura pertenece al artículo ID ' . $productId . ', pero el grupo actual es OC ' . $current['purchase_order'] . ' · ' . $expected . '. No cargues esta tarima en esta OC.');
    }

    $sameCode = trim((string)$label['label_code']);
    if ($sameCode !== '') {
      $stDup = $pdo->prepare("SELECT fi.id, fi.slot_no, fi.purchase_order, fi.product_label, fi.loaded_at, fo.id AS forklift_order_id, so.sales_order_ref FROM warehouse_forklift_load_order_items fi JOIN warehouse_forklift_load_orders fo ON fo.id=fi.forklift_order_id LEFT JOIN sales_orders so ON so.id=fo.order_id WHERE fo.company_id=? AND fo.location_id=? AND fi.loaded=1 AND fi.scanned_label_code=? LIMIT 1");
      $stDup->execute([$companyId, $locationId, $sameCode]);
      $dup = $stDup->fetch(PDO::FETCH_ASSOC);
      if ($dup) {
        $usedIn = (string)($dup['sales_order_ref'] ?? ('#' . $dup['forklift_order_id']));
        $dupMsg = 'Esta etiqueta ya está escaneada en la orden ' . $usedIn;
        if (!empty($dup['purchase_order'])) $dupMsg .= ' · OC ' . (string)$dup['purchase_order'];
        if (!empty($dup['slot_no'])) $dupMsg .= ' · espacio ' . (int)$dup['slot_no'];
        if (!empty($dup['loaded_at'])) $dupMsg .= ' · lectura ' . (string)$dup['loaded_at'];
        $dupMsg .= '. Puedes liberarla si fue una lectura incorrecta para volver a escanearla.';
        $pdo->rollBack();
        return [
          'duplicate'=>true,
          'message'=>$dupMsg,
          'label_code'=>$sameCode,
          'duplicate_item_id'=>(int)$dup['id'],
          'duplicate_order_id'=>(int)$dup['forklift_order_id'],
          'duplicate_sales_order_ref'=>$usedIn,
          'duplicate_purchase_order'=>(string)($dup['purchase_order'] ?? ''),
          'duplicate_slot_no'=>(int)($dup['slot_no'] ?? 0),
          'duplicate_loaded_at'=>(string)($dup['loaded_at'] ?? ''),
        ];
      }
    }

    $target = null;
    foreach ($items as $it) {
      $oc = trim((string)($it['purchase_order'] ?? 'SIN OC')) ?: 'SIN OC';
      $pl = trim((string)($it['product_label'] ?? 'SIN ARTÍCULO')) ?: 'SIN ARTÍCULO';
      $key = $oc . '||' . (int)$it['product_id'] . '||' . $pl;
      if ($key === $current['key'] && (int)$it['loaded'] !== 1) { $target = $it; break; }
    }
    if (!$target) throw new RuntimeException('No hay espacio pendiente para la OC/artículo actual.');

    $st = $pdo->prepare("UPDATE warehouse_forklift_load_order_items fi JOIN warehouse_forklift_load_orders fo ON fo.id=fi.forklift_order_id
      SET fi.loaded=1,
          fi.loaded_at=NOW(),
          fi.loaded_by=?,
          fi.scan_mode='SCANNER',
          fi.scanned_label_id=?,
          fi.scanned_label_code=?,
          fi.scanned_at=NOW(),
          fo.status=CASE WHEN fo.status='PENDIENTE' THEN 'EN_PROCESO' ELSE fo.status END,
          fo.load_started_by=CASE WHEN fo.load_started_by IS NULL THEN ? ELSE fo.load_started_by END,
          fo.load_started_at=CASE WHEN fo.load_started_at IS NULL THEN NOW() ELSE fo.load_started_at END
      WHERE fi.id=? AND fo.company_id=? AND fo.location_id=? AND fo.status IN ('PENDIENTE','EN_PROCESO')");
    $st->execute([$userId, $label['label_id'], $sameCode, $userId, (int)$target['id'], $companyId, $locationId]);

    $pdo->prepare("UPDATE sales_orders so JOIN warehouse_forklift_load_orders fo ON fo.order_id=so.id SET so.status='SURTIENDO', so.updated_at=NOW() WHERE fo.id=? AND fo.company_id=? AND fo.location_id=? AND so.outbound_id IS NULL")->execute([$forkliftOrderId,$companyId,$locationId]);
    $pdo->commit();

    return [
      'item_id'=>(int)$target['id'],
      'slot_no'=>(int)$target['slot_no'],
      'purchase_order'=>(string)$current['purchase_order'],
      'product_id'=>$productId,
      'product_label'=>(string)$current['product_label'],
      'label_code'=>$sameCode,
    ];
  } catch (Throwable $e) {
    if ($pdo->inTransaction()) $pdo->rollBack();
    throw $e;
  }
}

function fl_complete_summary(PDO $pdo, int $forkliftOrderId): array {
  $st=$pdo->prepare("SELECT fo.load_started_at, fo.confirmed_at, fo.vehicle_unit, fo.driver_name, so.sales_order_ref, c.business_name,
      ustart.full_name AS load_started_by_name,
      uconf.full_name AS confirmed_by_name
    FROM warehouse_forklift_load_orders fo
    JOIN sales_orders so ON so.id=fo.order_id
    JOIN sales_customers c ON c.id=so.customer_id
    LEFT JOIN users ustart ON ustart.id=fo.load_started_by
    LEFT JOIN users uconf ON uconf.id=fo.confirmed_by
    WHERE fo.id=? LIMIT 1");
  $st->execute([$forkliftOrderId]);
  $o=$st->fetch(PDO::FETCH_ASSOC)?:[];
  $st=$pdo->prepare("SELECT COUNT(*) AS slots, COALESCE(SUM(pallets_qty),0) AS pallets, COALESCE(SUM(packages_qty),0) AS packages, COALESCE(SUM(pieces_qty),0) AS pieces FROM warehouse_forklift_load_order_items WHERE forklift_order_id=?");
  $st->execute([$forkliftOrderId]);
  $t=$st->fetch(PDO::FETCH_ASSOC)?:[];
  $st=$pdo->prepare("SELECT purchase_order, product_label, customer_label, delivery_address, COUNT(*) AS slots, COALESCE(SUM(pallets_qty),0) AS pallets, COALESCE(SUM(packages_qty),0) AS packages, COALESCE(SUM(pieces_qty),0) AS pieces
    FROM warehouse_forklift_load_order_items
    WHERE forklift_order_id=?
    GROUP BY purchase_order, product_label, customer_label, delivery_address
    ORDER BY MIN(slot_no) ASC");
  $st->execute([$forkliftOrderId]);
  $groups=$st->fetchAll(PDO::FETCH_ASSOC);
  $start=(string)($o['load_started_at']??'');
  $end=(string)($o['confirmed_at']??'');
  $duration=0;
  if($start!=='' && $end!=='') {
    try { $duration=max(0,(new DateTimeImmutable($end))->getTimestamp()-(new DateTimeImmutable($start))->getTimestamp()); } catch(Throwable $e) { $duration=0; }
  }
  return [
    'order'=>$o,
    'totals'=>[
      'slots'=>(int)($t['slots']??0),
      'pallets'=>(float)($t['pallets']??0),
      'packages'=>(float)($t['packages']??0),
      'pieces'=>(float)($t['pieces']??0),
    ],
    'groups'=>$groups,
    'started_at'=>$start,
    'completed_at'=>$end,
    'duration_seconds'=>$duration,
    'duration_label'=>fl_fmt_duration($duration),
  ];
}

if($action!==''){
  fl_need_csrf();
  try{
    if($action==='orders') {
      $res=fl_orders($pdo,$companyId,$locationId,(string)($_GET['status']??''),(string)($_GET['date']??''),(int)($_GET['page']??1),(int)($_GET['per_page']??20));
      fl_jexit(['ok'=>true,'rows'=>$res['rows'],'meta'=>$res['meta']]);
    }
    if($action==='detail') { $id=(int)($_GET['id']??0); fl_jexit(['ok'=>true]+fl_detail($pdo,$companyId,$locationId,$id)); }
    if($action==='scan_label') {
      $d=fl_read_json();
      $id=(int)($d['id']??0);
      $label=trim((string)($d['label_code']??''));
      if($id<=0) throw new RuntimeException('Orden inválida.');
      $res=fl_scan_label($pdo,$companyId,$locationId,(int)($u['id']??0),$id,$label);
      fl_jexit(['ok'=>true,'scan'=>$res]);
    }
    if($action==='release_scanned_label') {
      $d=fl_read_json();
      $id=(int)($d['id']??0);
      $itemId=(int)($d['item_id']??0);
      $label=trim((string)($d['label_code']??''));
      if($id<=0) throw new RuntimeException('Orden inválida.');
      $res=fl_release_scanned_label($pdo,$companyId,$locationId,(int)($u['id']??0),$id,$label,$itemId);
      fl_jexit(['ok'=>true,'released'=>$res]);
    }
    if($action==='set_process') {
      $d=fl_read_json(); $id=(int)($d['id']??0); if($id<=0) throw new RuntimeException('Orden inválida.');
      $st=$pdo->prepare("UPDATE warehouse_forklift_load_orders
        SET status='EN_PROCESO', load_started_by=COALESCE(load_started_by, ?), load_started_at=COALESCE(load_started_at, NOW())
        WHERE id=? AND company_id=? AND location_id=? AND status IN ('PENDIENTE','EN_PROCESO')");
      $st->execute([(int)($u['id']??0),$id,$companyId,$locationId]);
      $pdo->prepare("UPDATE sales_orders so JOIN warehouse_forklift_load_orders fo ON fo.order_id=so.id SET so.status='SURTIENDO', so.updated_at=NOW() WHERE fo.id=? AND fo.company_id=? AND fo.location_id=? AND so.outbound_id IS NULL")->execute([$id,$companyId,$locationId]);
      fl_jexit(['ok'=>true,'started_at'=>date('Y-m-d H:i:s')]);
    }
    if($action==='toggle_item') {
      $d=fl_read_json(); $itemId=(int)($d['item_id']??0); $loaded=(int)($d['loaded']??0) ? 1 : 0; if($itemId<=0) throw new RuntimeException('Tarima inválida.');
      $st=$pdo->prepare("UPDATE warehouse_forklift_load_order_items fi JOIN warehouse_forklift_load_orders fo ON fo.id=fi.forklift_order_id
        SET fi.loaded=?, fi.loaded_at=CASE WHEN ?=1 THEN NOW() ELSE NULL END,
            fi.loaded_by=CASE WHEN ?=1 THEN ? ELSE NULL END,
            fi.scan_mode=CASE WHEN ?=1 THEN 'MANUAL' ELSE NULL END,
            fi.scanned_label_id=CASE WHEN ?=1 THEN fi.scanned_label_id ELSE NULL END,
            fi.scanned_label_code=CASE WHEN ?=1 THEN fi.scanned_label_code ELSE NULL END,
            fi.scanned_at=CASE WHEN ?=1 THEN fi.scanned_at ELSE NULL END,
            fo.status=CASE WHEN fo.status='PENDIENTE' THEN 'EN_PROCESO' ELSE fo.status END,
            fo.load_started_by=CASE WHEN ?=1 AND fo.load_started_by IS NULL THEN ? ELSE fo.load_started_by END,
            fo.load_started_at=CASE WHEN ?=1 AND fo.load_started_at IS NULL THEN NOW() ELSE fo.load_started_at END
        WHERE fi.id=? AND fo.company_id=? AND fo.location_id=? AND fo.status IN ('PENDIENTE','EN_PROCESO')");
      $st->execute([$loaded,$loaded,$loaded,(int)($u['id']??0),$loaded,$loaded,$loaded,$loaded,$loaded,(int)($u['id']??0),$loaded,$itemId,$companyId,$locationId]); fl_jexit(['ok'=>true]);
    }
    if($action==='complete') {
      $d=fl_read_json(); $id=(int)($d['id']??0); $notes=trim((string)($d['notes']??'')); if($id<=0) throw new RuntimeException('Orden inválida.');
      $pdo->beginTransaction();
      $st=$pdo->prepare("SELECT * FROM warehouse_forklift_load_orders WHERE id=? AND company_id=? AND location_id=? FOR UPDATE"); $st->execute([$id,$companyId,$locationId]); $ord=$st->fetch(PDO::FETCH_ASSOC); if(!$ord) throw new RuntimeException('Orden no encontrada.');
      if(strtoupper((string)$ord['status'])==='COMPLETA') throw new RuntimeException('La orden ya está completa.');
      $st=$pdo->prepare("SELECT COUNT(*) AS total, SUM(CASE WHEN loaded=1 THEN 1 ELSE 0 END) AS loaded FROM warehouse_forklift_load_order_items WHERE forklift_order_id=?"); $st->execute([$id]); $r=$st->fetch(PDO::FETCH_ASSOC)?:[];
      if((int)($r['total']??0)<=0) throw new RuntimeException('La orden no tiene tarimas.');
      if((int)($r['loaded']??0) < (int)($r['total']??0)) throw new RuntimeException('Aún no están marcadas todas las tarimas como cargadas.');
      $piecesCheck = fl_loaded_pieces_check($pdo, $id);
      if (empty($piecesCheck['ok'])) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        fl_jexit([
          'ok'=>false,
          'error'=>'La carga no puede finalizarse: faltan piezas contra la cantidad requerida por la OC.',
          'error_code'=>'PIECES_SHORTAGE',
          'pieces_check'=>$piecesCheck,
        ], 409);
      }
      $pdo->prepare("UPDATE warehouse_forklift_load_orders SET status='COMPLETA', load_started_by=COALESCE(load_started_by, ?), load_started_at=COALESCE(load_started_at, created_at), confirmed_by=?, confirmed_at=NOW(), completed_notes=? WHERE id=?")->execute([(int)($u['id']??0), (int)($u['id']??0), $notes!==''?$notes:null, $id]);
      if(fl_table_exists($pdo,'sales_truck_load_plans') && (int)$ord['truck_plan_id'] > 0) $pdo->prepare("UPDATE sales_truck_load_plans SET status='FORKLIFT_COMPLETE', updated_by=?, updated_at=NOW() WHERE id=?")->execute([(int)($u['id']??0),(int)$ord['truck_plan_id']]);
      $stSo=$pdo->prepare("SELECT status FROM sales_orders WHERE id=? AND company_id=? AND location_id=? FOR UPDATE");
      $stSo->execute([(int)$ord['order_id'],$companyId,$locationId]);
      $oldSalesStatus=(string)($stSo->fetchColumn() ?: '');
      $pdo->prepare("UPDATE sales_orders SET status='AUTORIZADA', updated_at=NOW() WHERE id=? AND company_id=? AND location_id=?")->execute([(int)$ord['order_id'],$companyId,$locationId]);
      if(fl_table_exists($pdo,'sales_order_status_log')) {
        $pdo->prepare("INSERT INTO sales_order_status_log(order_id, old_status, new_status, changed_by_user_id, changed_by_role, comment, created_at) VALUES(?, ?, 'AUTORIZADA', ?, 'MONTACARGAS', ?, NOW())")->execute([(int)$ord['order_id'], $oldSalesStatus !== '' ? $oldSalesStatus : null, (int)($u['id']??0), 'Carga completa confirmada por montacargas. Pedido listo para surtir desde Almacén.']);
      }
      $pdo->commit(); fl_jexit(['ok'=>true,'summary'=>fl_complete_summary($pdo,$id)]);
    }
    fl_jexit(['ok'=>false,'error'=>'Acción no válida'],400);
  }catch(Throwable $e){ if($pdo->inTransaction()) $pdo->rollBack(); fl_jexit(['ok'=>false,'error'=>$e->getMessage()],400); }
}
$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 · Montacargas 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>
:root{
  --brand:#0f4c81;
  --brand-2:#1d75d8;
  --ink:#10243e;
  --muted:#64748b;
  --line:#dbe5f1;
  --soft:#f4f8fc;
  --ok:#16a34a;
  --warn:#f59e0b;
  --info:#2563eb;
  --danger:#dc2626;
  --shadow:0 18px 42px rgba(15,35,65,.10);
}
*{box-sizing:border-box}
html,body{min-height:100%}
body{
  margin:0;
  background:
    radial-gradient(circle at top left,rgba(29,117,216,.12),transparent 32rem),
    linear-gradient(180deg,#f7fbff 0%,#eef3f8 100%);
  color:var(--ink);
  font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
}
.app-hero{
  position:sticky;
  top:0;
  z-index:100;
  background:linear-gradient(135deg,#08213f 0%,#0f4c81 48%,#1d75d8 100%);
  color:#fff;
  box-shadow:0 14px 36px rgba(8,33,63,.22);
}
.hero-inner{
  max-width:1480px;
  margin:0 auto;
  padding:18px clamp(14px,2vw,24px);
  display:flex;
  gap:14px;
  align-items:center;
  justify-content:space-between;
}
.hero-title{display:flex;gap:12px;align-items:center;min-width:0}
.hero-icon{
  width:52px;height:52px;border-radius:18px;
  display:flex;align-items:center;justify-content:center;
  background:rgba(255,255,255,.16);
  border:1px solid rgba(255,255,255,.20);
  font-size:26px;flex:0 0 auto;
}
.hero-copy{min-width:0}
.eyebrow{font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;opacity:.76;font-weight:800}
h1{font-size:clamp(1.35rem,3vw,2.15rem);line-height:1.05;margin:0;font-weight:900;letter-spacing:-.03em}
.hero-sub{opacity:.80;font-size:.93rem;margin-top:4px;max-width:740px}
.hero-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end}
.hero-actions .btn{border-radius:999px;font-weight:800;padding:.55rem .85rem;white-space:nowrap}
.shell{max-width:1480px;margin:0 auto;padding:18px clamp(12px,2vw,24px) 28px}
.quickbar{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:14px}
.kpi-card{
  border:1px solid rgba(15,76,129,.10);
  border-radius:20px;
  background:rgba(255,255,255,.86);
  backdrop-filter:blur(10px);
  box-shadow:var(--shadow);
  padding:14px;
}
.kpi-label{font-size:.76rem;color:var(--muted);font-weight:800;text-transform:uppercase;letter-spacing:.05em}
.kpi-value{font-size:1.45rem;line-height:1;font-weight:950;letter-spacing:-.04em;margin-top:5px}
.kpi-hint{font-size:.76rem;color:var(--muted);margin-top:4px}
.glass{
  background:rgba(255,255,255,.92);
  border:1px solid rgba(15,76,129,.12);
  border-radius:24px;
  box-shadow:var(--shadow);
  overflow:hidden;
}
.panel-pad{padding:18px}
.section-head{
  display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px;
}
.section-title{font-size:1rem;font-weight:900;letter-spacing:-.01em;display:flex;gap:8px;align-items:center}
.muted{color:var(--muted)}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.btn{border-radius:13px;font-weight:800}.btn-sm{border-radius:11px}
.layout{display:grid;grid-template-columns:390px 1fr;gap:16px;align-items:start}
.orders-panel{position:sticky;top:92px}
.order-card{
  border:1px solid var(--line);
  border-radius:18px;
  padding:14px;
  cursor:pointer;
  background:#fff;
  transition:transform .14s ease,box-shadow .14s ease,border-color .14s ease,background .14s ease;
  position:relative;overflow:hidden;
}
.order-card:before{content:"";position:absolute;left:0;top:0;bottom:0;width:5px;background:#cbd5e1}
.order-card:hover{transform:translateY(-2px);box-shadow:0 14px 26px rgba(15,35,65,.10)}
.order-card.active{border-color:var(--brand-2);box-shadow:0 0 0 4px rgba(29,117,216,.12),0 16px 32px rgba(15,35,65,.12)}
.order-card.active:before{background:var(--brand-2)}
.order-card.border-primary:before{background:var(--info)}
.order-card.border-success:before{background:var(--ok)}
.order-main{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}
.status-chip,.badge-status{font-size:.73rem;border-radius:999px;padding:.32rem .56rem;font-weight:900;letter-spacing:.02em}
.progress{background:#e9eff6;border-radius:999px;overflow:hidden}
.detail-empty{
  min-height:420px;display:flex;align-items:center;justify-content:center;text-align:center;color:var(--muted);
  background:linear-gradient(135deg,#fff,#f7fbff);
}
.detail-empty i{font-size:52px;color:#93c5fd;display:block;margin-bottom:10px}
.summary-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:14px}
.summary-tile{
  padding:14px;border-radius:18px;background:linear-gradient(180deg,#f8fbff,#f1f6fc);border:1px solid #e2edf8;
}
.summary-tile .small{font-weight:800;text-transform:uppercase;letter-spacing:.04em}.summary-tile b{font-size:1.05rem}
.group-card{
  border:1px solid var(--line);
  border-radius:18px;
  padding:14px;
  background:#fff;
  transition:.15s ease;
}
.group-card.current{background:linear-gradient(135deg,#eff6ff,#fff);border-color:#60a5fa;box-shadow:0 0 0 4px rgba(37,99,235,.10)}
.group-card.done{background:linear-gradient(135deg,#ecfdf5,#fff);border-color:#86efac}
.group-card.pending{background:#f8fafc;color:#64748b}
.counter-pill{display:inline-flex;align-items:center;gap:.35rem;border-radius:999px;padding:.38rem .68rem;font-weight:900;font-size:.78rem;white-space:nowrap}
.counter-pending{background:#f1f5f9;color:#334155}.counter-current{background:#dbeafe;color:#1d4ed8}.counter-done{background:#dcfce7;color:#166534}
.work-note{border-radius:18px;border:1px solid #bfdbfe;background:#eff6ff;color:#1e3a8a;padding:12px;margin-bottom:14px;font-weight:650}
.slot-card{
  border:1px solid var(--line);
  border-radius:18px;
  padding:14px;
  background:#fff;
  transition:.15s ease;
  min-height:178px;
}
.slot-card.loaded{background:linear-gradient(135deg,#ecfdf5,#fff);border-color:#86efac}
.slot-card.current{background:linear-gradient(135deg,#fff7ed,#fff);border-color:#fb923c;box-shadow:0 0 0 4px rgba(251,146,60,.12)}
.slot-card.blocked{background:#f8fafc;color:#64748b}
.slot-card.group-done{background:#ecfdf5;border-color:#86efac}
.slot-badge{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:.24rem .55rem;font-size:.72rem;font-weight:900}
.slot-badge.current{background:#dbeafe;color:#1d4ed8}.slot-badge.done{background:#dcfce7;color:#166534}.slot-badge.locked{background:#e2e8f0;color:#475569}
.form-switch .form-check-input{width:3.2rem;height:1.65rem;cursor:pointer}.form-switch .form-check-input:checked{background-color:var(--ok);border-color:var(--ok)}
.action-bar{position:sticky;bottom:12px;z-index:20;margin-top:14px;display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end;background:rgba(255,255,255,.86);backdrop-filter:blur(10px);border:1px solid rgba(15,76,129,.12);border-radius:18px;padding:10px;box-shadow:0 12px 32px rgba(15,35,65,.12)}
.load-control-card{border:1px solid #bfdbfe;background:linear-gradient(135deg,#eff6ff,#fff);border-radius:20px;padding:14px;margin-bottom:14px;display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap}.load-control-card.started{border-color:#86efac;background:linear-gradient(135deg,#ecfdf5,#fff)}.time-pill{display:inline-flex;align-items:center;gap:.35rem;border-radius:999px;background:#f1f5f9;color:#334155;padding:.34rem .62rem;font-weight:900;font-size:.78rem}.summary-list{max-height:260px;overflow:auto;border:1px solid #e2e8f0;border-radius:14px}.summary-row{padding:10px 12px;border-bottom:1px solid #e2e8f0}.summary-row:last-child{border-bottom:0}
.scanner-panel{border:1px solid #bfdbfe;background:linear-gradient(135deg,#eff6ff,#fff);border-radius:18px;padding:12px;margin-bottom:14px}.scanner-panel.scanner-active{border-color:#22c55e;background:linear-gradient(135deg,#ecfdf5,#fff)}.scanner-input{font-size:1.1rem;font-weight:800;letter-spacing:.03em}.mode-toggle .btn{border-radius:999px}.scan-hint{font-size:.8rem;color:#64748b}

.floating-scanner{position:fixed;left:50%;bottom:16px;transform:translateX(-50%) translateY(120%);width:min(860px,calc(100vw - 24px));z-index:1800;opacity:0;pointer-events:none;transition:transform .18s ease,opacity .18s ease}.floating-scanner.show{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}.floating-scanner-card{border:2px solid #22c55e;background:linear-gradient(135deg,#ecfdf5,#ffffff);box-shadow:0 22px 65px rgba(15,23,42,.28);border-radius:22px;padding:12px}.floating-scanner-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.floating-scanner-title{font-weight:950;color:#14532d}.floating-scanner-expected{font-size:.82rem;color:#1e3a8a;font-weight:800;overflow-wrap:anywhere}.floating-scanner-input{font-size:1.25rem;font-weight:950;letter-spacing:.045em;border:2px solid #86efac!important;border-radius:16px!important;min-height:54px}.floating-scanner-input:focus{box-shadow:0 0 0 .22rem rgba(34,197,94,.20)!important}.floating-scanner .btn{min-height:50px}.scan-error-strong{border:1px solid #fecaca;background:#fff1f2;color:#991b1b;border-radius:14px;padding:10px}.scan-ok-strong{border:1px solid #bbf7d0;background:#f0fdf4;color:#166534;border-radius:14px;padding:10px}@media(max-width:767.98px){.floating-scanner{bottom:8px;width:calc(100vw - 14px)}.floating-scanner-card{border-radius:18px;padding:10px}.floating-scanner-head{align-items:flex-start}.floating-scanner-title{font-size:.95rem}.floating-scanner-input{font-size:1rem;min-height:48px}.floating-scanner .btn{min-height:44px}.floating-scanner .scan-action-text{display:none}}

.camera-scan-card{border:1px solid #bfdbfe;background:linear-gradient(135deg,#eff6ff,#ffffff);border-radius:16px;padding:12px;margin-top:10px}.camera-scan-video-wrap{position:relative;overflow:hidden;border-radius:18px;background:#020617;border:1px solid #0f172a;min-height:280px;display:flex;align-items:center;justify-content:center}.camera-scan-video{width:100%;max-height:56vh;object-fit:cover;background:#020617}.camera-scan-frame{position:absolute;inset:18%;border:3px solid rgba(34,197,94,.95);border-radius:18px;box-shadow:0 0 0 999px rgba(2,6,23,.35)}.camera-scan-line{position:absolute;left:20%;right:20%;top:50%;height:3px;background:#22c55e;box-shadow:0 0 18px #22c55e;border-radius:999px;animation:scanLine 1.35s ease-in-out infinite}@keyframes scanLine{0%,100%{transform:translateY(-60px)}50%{transform:translateY(60px)}}.camera-status{font-size:.88rem;color:#475569}.camera-read-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#f8fafc;border:1px dashed #cbd5e1;border-radius:12px;padding:8px;word-break:break-all}.btn-camera{background:#ecfeff;border-color:#67e8f9;color:#155e75}.btn-camera:hover{background:#cffafe;color:#164e63}@media(max-width:767.98px){.camera-scan-video-wrap{min-height:340px}.camera-scan-frame{inset:20% 12%}.camera-scan-line{left:14%;right:14%}}
.app-modal-backdrop{position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:2000;display:none;align-items:center;justify-content:center;padding:18px}
.app-modal-card{width:min(560px,100%);background:#fff;border-radius:22px;border:1px solid #dbe5f1;box-shadow:0 20px 60px rgba(15,23,42,.25);overflow:hidden}


.truck-stage{
  display:grid;
  grid-template-columns:132px 1fr;
  gap:12px;
  align-items:stretch;
  margin-top:12px;
}
.truck-cab{
  border:2px solid #93c5fd;
  background:linear-gradient(135deg,#dbeafe,#ffffff);
  border-radius:26px 16px 16px 26px;
  min-height:180px;
  display:flex;
  flex-direction:column;
  justify-content:center;
  align-items:center;
  text-align:center;
  position:relative;
  box-shadow:inset 0 0 0 5px rgba(255,255,255,.55);
}
.truck-cab:before,.truck-cab:after{
  content:"";position:absolute;right:-17px;width:26px;height:26px;border-radius:999px;background:#0f4c81;border:5px solid #e2e8f0;
}
.truck-cab:before{top:34px}.truck-cab:after{bottom:34px}
.truck-cab i{font-size:42px;color:#0f4c81;margin-bottom:8px}
.truck-bed{
  border:2px dashed #93c5fd;
  border-radius:18px;
  padding:12px;
  background:linear-gradient(180deg,#f8fbff,#eef6ff);
  position:relative;
}
.truck-bed:after{
  content:"";position:absolute;left:24px;right:24px;bottom:-14px;height:8px;border-radius:999px;background:linear-gradient(90deg,#0f4c81 0 18%,transparent 18% 34%,#0f4c81 34% 52%,transparent 52% 70%,#0f4c81 70% 88%,transparent 88%);
  opacity:.45;
}
.truck-direction{
  display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px;color:#1e3a8a;font-weight:900;font-size:.82rem;
}
.truck-direction .line{height:2px;background:#93c5fd;flex:1;border-radius:999px}

#detailBox{width:100%;min-width:0;}
.detail-loaded{display:block;width:100%;min-width:0;}
.detail-top{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:14px;}
.detail-title{min-width:0;}
.detail-title h4{word-break:break-word;}
.slots-board{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px;align-items:stretch;}
.slot-card{overflow:hidden;word-break:break-word;}
.slot-card .fw-semibold,.slot-card .small,.slot-card .mono{overflow-wrap:anywhere;}
.slot-title-row{display:flex;justify-content:space-between;gap:8px;align-items:flex-start;}
.slot-product{font-weight:850;color:#0f2a55;margin-top:6px;}
.mobile-current-banner{display:none;border:1px solid #bfdbfe;background:linear-gradient(135deg,#eff6ff,#ffffff);border-radius:18px;padding:12px;margin-bottom:12px;color:#1e3a8a;}
@media (max-width:767.98px){
  .detail-top{display:block}.detail-top .badge-status{display:inline-block;margin-top:8px}
  .slots-board{grid-template-columns:1fr;gap:9px}
  .slot-card{border-radius:16px;padding:12px;min-height:auto}
  .slot-title-row{align-items:center}
  .form-switch .form-check-input{width:3rem;height:1.55rem}
  .mobile-current-banner{display:block}
  .group-card{border-radius:16px;padding:12px}
}
@media (max-width:420px){.slots-board{grid-template-columns:minmax(0,1fr)}.slot-card{font-size:.92rem}}

@media (max-width:1199.98px){
  .layout{grid-template-columns:1fr}.orders-panel{position:relative;top:auto}.orders-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.quickbar{grid-template-columns:repeat(2,minmax(0,1fr))}.hero-inner{align-items:flex-start}.hero-actions{width:100%;justify-content:flex-start}.hero-sub{font-size:.86rem}
}
@media (max-width:767.98px){
  .app-hero{position:relative}.hero-inner{padding:12px;gap:10px}.hero-icon{width:42px;height:42px;border-radius:14px;font-size:22px}.eyebrow{font-size:.68rem}h1{font-size:1.28rem}.hero-sub{display:none}.hero-actions .btn{padding:.48rem .62rem;font-size:.82rem}.shell{padding:12px}.quickbar{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.kpi-card{padding:11px;border-radius:16px}.kpi-value{font-size:1.18rem}.layout{gap:12px}.glass{border-radius:18px}.panel-pad{padding:12px}.orders-grid{grid-template-columns:1fr}.summary-grid{grid-template-columns:1fr}.slot-card{min-height:auto}.action-bar{position:relative;bottom:auto;justify-content:stretch}.action-bar .btn{width:100%}
}
@media (max-width:420px){.quickbar{grid-template-columns:1fr}.hero-actions{gap:6px}.hero-actions .btn{font-size:.78rem}.order-main{display:block}.badge-status{margin-top:6px}.section-head{align-items:flex-start}}


/* ============================================================
   CABECERO COMPACTO RESPONSIVO · MONTACARGAS
   La información secundaria vive en el botón de usuario/KPIs.
============================================================ */
.app-hero-compact{position:sticky;top:0;z-index:100;background:linear-gradient(135deg,#08213f 0%,#0f4c81 65%,#1d75d8 100%);box-shadow:0 10px 26px rgba(8,33,63,.22)}
.hero-inner-compact{max-width:1480px;margin:0 auto;padding:10px clamp(10px,2vw,18px);display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:58px}
.compact-brand{display:flex;align-items:center;gap:10px;min-width:0}.profile-btn{width:44px;height:44px;border-radius:16px;border:1px solid rgba(255,255,255,.22);background:rgba(255,255,255,.14);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.55rem;box-shadow:inset 0 0 0 1px rgba(255,255,255,.05);cursor:pointer;flex:0 0 auto}.profile-btn:active{transform:scale(.98)}
.compact-title{min-width:0}.compact-eyebrow{font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;opacity:.78;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.compact-main{font-size:1.18rem;line-height:1.05;font-weight:950;letter-spacing:-.035em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.compact-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex:0 0 auto}.compact-action-btn{border-radius:14px!important;min-height:42px;font-weight:900!important;padding:.48rem .72rem!important;gap:.35rem}.compact-action-btn i{font-size:1.05rem}
.filter-card-compact{padding:12px 14px!important}.filter-line{display:flex;align-items:center;justify-content:space-between;gap:12px}.filter-title{min-width:0}.filter-controls{display:grid;grid-template-columns:170px auto;gap:8px;align-items:center}.filter-controls .form-control,.filter-controls .btn{min-height:42px;border-radius:13px;font-weight:850}
.info-card-mini{display:flex;gap:12px;align-items:flex-start;border:1px solid #dbe5f1;background:linear-gradient(135deg,#f8fbff,#fff);border-radius:18px;padding:12px}.info-icon{width:42px;height:42px;border-radius:14px;background:#eef6ff;color:#0f4c81;display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex:0 0 auto}.btn-danger-soft{background:#fff1f2!important;color:#be123c!important;border:1px solid #fecdd3!important;font-weight:900!important}.user-modal-actions{display:flex;justify-content:space-between!important;gap:8px}.user-modal-actions .btn{min-width:135px}.app-info-modal{border-radius:22px;overflow:hidden;border:1px solid #dbe5f1}.kpi-modal-grid{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:0!important}.kpi-modal-grid .kpi-card{box-shadow:none;background:#f8fbff}
@media (max-width:1199.98px){.hero-inner-compact{min-height:54px;padding:8px 12px}.compact-main{font-size:1.05rem}.compact-action-btn span{display:none}.compact-action-btn{width:44px;padding:.45rem!important}.filter-line{align-items:stretch}.filter-controls{grid-template-columns:1fr auto}.kpi-modal-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.shell{padding-top:10px}.orders-panel{position:relative;top:auto}}
@media (max-width:767.98px){.app-hero{position:sticky}.hero-inner-compact{min-height:48px;padding:6px 9px}.profile-btn{width:40px;height:40px;border-radius:14px;font-size:1.4rem}.compact-eyebrow{display:none}.compact-main{font-size:1rem;max-width:42vw}.compact-actions{gap:6px}.compact-action-btn{width:40px;min-height:40px!important;border-radius:13px!important}.filter-card-compact{padding:10px!important;border-radius:18px!important}.filter-line{display:block}.filter-title{display:none}.filter-controls{grid-template-columns:1fr auto;gap:7px}.filter-controls .form-control,.filter-controls .btn{min-height:40px}.filter-controls .btn{width:auto;padding-left:.75rem;padding-right:.75rem}.kpi-modal-grid{grid-template-columns:1fr}.user-modal-actions{display:grid!important;grid-template-columns:1fr;}.user-modal-actions .btn{width:100%}.shell{padding:10px}.layout{gap:10px}.glass{border-radius:18px}.panel-pad{padding:12px!important}}
@media (orientation:landscape) and (max-height:540px){.hero-inner-compact{min-height:44px;padding:5px 10px}.profile-btn{width:36px;height:36px;border-radius:12px}.compact-main{font-size:.96rem}.compact-action-btn{width:38px;min-height:36px!important}.shell{padding-top:8px}.filter-card-compact{padding:8px 10px!important}.filter-controls .form-control,.filter-controls .btn{min-height:36px}.orders-panel{top:auto}}

.load-info-btn{border-radius:999px!important;font-weight:900!important;box-shadow:0 10px 22px rgba(37,99,235,.12)}
.load-info-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
.load-info-box{border:1px solid #e2e8f0;background:#f8fafc;border-radius:16px;padding:11px}
.load-info-box .label{font-size:.72rem;color:#64748b;text-transform:uppercase;letter-spacing:.05em;font-weight:900}
.load-info-box .value{font-weight:900;overflow-wrap:anywhere}
.truck-stage.compact-only{display:block}.truck-stage.compact-only .truck-bed{min-height:160px}
@media(max-width:767.98px){.load-info-grid{grid-template-columns:1fr}.load-info-btn{width:100%}}

</style></head><body>
<header class="app-hero app-hero-compact">
  <div class="hero-inner hero-inner-compact">
    <div class="compact-brand">
      <button class="profile-btn" type="button" data-bs-toggle="modal" data-bs-target="#userInfoModal" aria-label="Información de usuario">
        <i class="bi bi-person-circle"></i>
      </button>
      <div class="compact-title">
        <div class="compact-eyebrow">BReport · Botella</div>
        <div class="compact-main">Montacargas</div>
      </div>
    </div>
    <div class="compact-actions">
      <button class="btn btn-light compact-action-btn" id="btnReloadTop" type="button"><i class="bi bi-arrow-clockwise"></i><span>Actualizar</span></button>
      <button class="btn btn-light compact-action-btn" type="button" data-bs-toggle="modal" data-bs-target="#kpiTotalsModal"><i class="bi bi-graph-up-arrow"></i><span>KPIs</span></button>
    </div>
  </div>
</header>

<div class="modal fade" id="userInfoModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content app-info-modal">
      <div class="modal-header">
        <div>
          <h5 class="modal-title"><i class="bi bi-truck-front-fill me-2 text-primary"></i>Montacargas Botella</h5>
          <div class="small text-muted">Órdenes de carga · Confirmación física por OC y artículo</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
      </div>
      <div class="modal-body">
        <div class="info-card-mini mb-2">
          <div class="info-icon"><i class="bi bi-person-badge"></i></div>
          <div>
            <div class="small text-muted">Usuario en sesión</div>
            <div class="fw-bold" id="currentUserLabel"><?=fl_h((string)($u['full_name'] ?? $u['username'] ?? $u['email'] ?? 'Usuario'))?></div>
          </div>
        </div>
        <div class="info-card-mini mb-2">
          <div class="info-icon"><i class="bi bi-shield-check"></i></div>
          <div>
            <div class="small text-muted">Trazabilidad</div>
            <div class="fw-bold">Inicio, tarimas cargadas y cierre quedan ligados al usuario.</div>
          </div>
        </div>
        <div class="info-card-mini">
          <div class="info-icon"><i class="bi bi-info-circle"></i></div>
          <div>
            <div class="small text-muted">Nota operativa</div>
            <div class="fw-bold">Esta pantalla no descuenta inventario; solo confirma carga física.</div>
          </div>
        </div>
      </div>
      <div class="modal-footer user-modal-actions">
        <a class="btn btn-outline-primary" href="dashboard.php"><i class="bi bi-grid me-1"></i>Dashboard</a>
        <a class="btn btn-danger-soft" href="logout.php"><i class="bi bi-box-arrow-right me-1"></i>Salir</a>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="kpiTotalsModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered modal-lg">
    <div class="modal-content app-info-modal">
      <div class="modal-header">
        <div>
          <h5 class="modal-title"><i class="bi bi-graph-up-arrow me-2 text-primary"></i>KPI / Totales</h5>
          <div class="small text-muted">Resumen rápido de órdenes, tarimas y avance.</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
      </div>
      <div class="modal-body">
        <section class="quickbar kpi-modal-grid mb-0">
          <div class="kpi-card"><div class="kpi-label">Órdenes</div><div class="kpi-value" id="kOrders">0</div><div class="kpi-hint">Pendientes y en proceso</div></div>
          <div class="kpi-card"><div class="kpi-label">Tarimas</div><div class="kpi-value" id="kPallets">0</div><div class="kpi-hint">Solicitadas para cargar</div></div>
          <div class="kpi-card"><div class="kpi-label">Avance</div><div class="kpi-value" id="kProgress">0%</div><div class="kpi-hint">Tarimas confirmadas</div></div>
          <div class="kpi-card"><div class="kpi-label">Siguiente acción</div><div class="kpi-value" id="kNext" style="font-size:1rem;line-height:1.2">Seleccionar orden</div><div class="kpi-hint">Flujo guiado</div></div>
        </section>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="loadInfoModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered modal-lg">
    <div class="modal-content app-info-modal">
      <div class="modal-header">
        <div>
          <h5 class="modal-title"><i class="bi bi-info-circle me-2 text-primary"></i>Información de la carga</h5>
          <div class="small text-muted">Datos completos de la orden seleccionada.</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
      </div>
      <div class="modal-body" id="loadInfoModalBody">
        <div class="text-muted">Selecciona una orden para ver la información.</div>
      </div>
    </div>
  </div>
</div>

<main class="shell">


  <section class="glass panel-pad mb-3 filter-card-compact">
    <div class="filter-line">
      <div class="filter-title">
        <div class="section-title mb-1"><i class="bi bi-calendar-check text-primary"></i> Consulta de órdenes</div>
        <div class="small muted">Selecciona fecha y actualiza la bandeja.</div>
      </div>
      <div class="filter-controls">
        <input type="date" class="form-control" id="filterDate" value="<?=date('Y-m-d')?>" aria-label="Fecha de órdenes">
        <button class="btn btn-primary" id="btnFilterDate" type="button"><i class="bi bi-search me-1"></i>Consultar</button>
      </div>
    </div>
  </section>

  <section class="layout">
    <aside class="orders-panel">
      <div class="glass panel-pad">
        <div class="section-head">
          <div>
            <div class="section-title"><i class="bi bi-list-check text-primary"></i> Bandeja de carga</div>
            <div class="small muted">Selecciona una orden y sigue el orden sugerido.</div>
          </div>
          <button class="btn btn-outline-primary btn-sm" id="btnReload" type="button"><i class="bi bi-arrow-clockwise"></i></button>
        </div>
        <div id="ordersList" class="orders-grid d-grid gap-2"></div>
        <div id="ordersPager" class="d-flex justify-content-between align-items-center gap-2 mt-3 flex-wrap"></div>
      </div>
    </aside>
    <section>
      <div class="glass panel-pad">
        <div id="detailBox" class="detail-empty">
          <div>
            <i class="bi bi-truck"></i>
            <div class="fw-bold fs-5">Selecciona una orden para cargar</div>
            <div class="small">El sistema te indicará qué OC y artículo deben cargarse primero.</div>
          </div>
        </div>
      </div>
    </section>
  </section>
</main>
<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 CURRENT_USER=<?=json_encode((string)($u['full_name'] ?? $u['username'] ?? $u['email'] ?? 'Usuario'), JSON_UNESCAPED_UNICODE)?>;
const $=id=>document.getElementById(id);
let rows=[], selected=null, lastCompletedNoticeKey='', currentPage=1, pageMeta={date:'',page:1,pages:1,total:0,per_page:20};
let loadInputMode='manual';
let scannerBusy=false;
let lastScannerCode='', lastScannerAt=0;
let cameraStream=null,cameraLoopActive=false,cameraDetector=null,cameraLastCode='',cameraLastAt=0;
const h=s=>String(s??'').replace(/[&<>\"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));
const n=v=>Number(v||0);
const fmt=v=>Number(v||0).toLocaleString('es-MX',{maximumFractionDigits:0});
const fmtDateTime=s=>s?String(s).replace('T',' ').slice(0,19):'—';
function durationLabel(seconds){seconds=Math.max(0,Number(seconds||0));const h=Math.floor(seconds/3600),m=Math.floor((seconds%3600)/60),sec=Math.floor(seconds%60);if(h>0)return `${h}h ${String(m).padStart(2,'0')}m ${String(sec).padStart(2,'0')}s`;if(m>0)return `${m}m ${String(sec).padStart(2,'0')}s`;return `${sec}s`;}
function elapsedFrom(start){ if(!start) return ''; const t=new Date(String(start).replace(' ','T')).getTime(); if(!Number.isFinite(t)) return ''; return durationLabel((Date.now()-t)/1000); }

function fillLoadInfoModal(groups=[], curKey='', items=[]){
  if(!selected || !$('loadInfoModalBody')) return;
  const current=groups.find(g=>g.key===curKey) || null;
  const totalSlots=items.length;
  const loadedSlots=items.filter(it=>Number(it.loaded)).length;
  const totalPackages=items.reduce((a,it)=>a+n(it.packages_qty),0);
  const totalPieces=items.reduce((a,it)=>a+n(it.pieces_qty),0);
  const started=String(selected.status||'').toUpperCase()!=='PENDIENTE' || !!selected.load_started_at;
  $('loadInfoModalBody').innerHTML=`
    <div class="load-info-grid mb-3">
      <div class="load-info-box"><div class="label">Orden</div><div class="value mono">#${h(selected.id)} · ${h(selected.sales_order_ref||'')}</div></div>
      <div class="load-info-box"><div class="label">Estado</div><div class="value">${statusBadge(selected.status)}</div></div>
      <div class="load-info-box"><div class="label">Cliente</div><div class="value">${h(selected.business_name||selected.contact_name||'—')}</div></div>
      <div class="load-info-box"><div class="label">Unidad</div><div class="value">${h(selected.vehicle_unit||'—')}</div></div>
      <div class="load-info-box"><div class="label">Chofer</div><div class="value">${h(selected.driver_name||'—')}</div></div>
      <div class="load-info-box"><div class="label">Montacarguista</div><div class="value">${h(selected.confirmed_by_name||selected.load_started_by_name||CURRENT_USER||'—')}</div></div>
      <div class="load-info-box"><div class="label">Ruta</div><div class="value">${h(selected.route_no||'—')}</div></div>
      <div class="load-info-box"><div class="label">OC base</div><div class="value mono">${h(selected.purchase_order||'—')}</div></div>
      <div class="load-info-box"><div class="label">Entrega</div><div class="value">${h(selected.requested_delivery_date||'—')} ${h(selected.requested_delivery_time||'')}</div></div>
      <div class="load-info-box"><div class="label">Dirección</div><div class="value">${h(selected.delivery_address||'—')}</div></div>
      <div class="load-info-box"><div class="label">Tiempo de carga</div><div class="value">${started ? h(elapsedFrom(selected.load_started_at)) : 'Sin iniciar'}</div><div class="small text-muted">Inicio: ${h(fmtDateTime(selected.load_started_at))}</div></div>
      <div class="load-info-box"><div class="label">Avance</div><div class="value">${fmt(loadedSlots)} / ${fmt(totalSlots)} tarimas</div><div class="small text-muted">${fmt(totalPackages)} paquetes · ${fmt(totalPieces)} piezas</div></div>
    </div>
    ${current?`<div class="alert alert-primary"><b>Carga actual:</b> OC ${h(current.purchase_order)} · ${h(current.product_label)}<br><span class="small">Completa este grupo antes de continuar con el siguiente.</span></div>`:''}
    <div class="fw-bold mb-2"><i class="bi bi-list-check me-1"></i>Resumen por OC y artículo</div>
    <div class="summary-list">${groups.map(g=>`<div class="summary-row"><div class="d-flex justify-content-between gap-2"><div><b>OC ${h(g.purchase_order)}</b><br><span class="small text-muted">${h(g.product_label)}</span></div><div class="text-end"><b>${fmt(g.loaded)}/${fmt(g.total)}</b><br><span class="small text-muted">${fmt(g.packages)} paq · ${fmt(g.pieces)} pzas</span></div></div></div>`).join('') || '<div class="summary-row text-muted">Sin grupos.</div>'}</div>
  `;
}
function showLoadInfoModal(){
  const el=$('loadInfoModal');
  if(!el){ appAlert('No se encontró el modal de información.'); return; }
  el.addEventListener('hidden.bs.modal',()=>restoreScannerFocus(180),{once:true});
  bootstrap.Modal.getOrCreateInstance(el).show();
}


function ensureAppModal(){
  if(document.getElementById('appModal')) return;
  const div=document.createElement('div');
  div.id='appModal';
  div.className='app-modal-backdrop';
  div.innerHTML=`<div class="app-modal-card">
    <div class="p-3 border-bottom d-flex justify-content-between align-items-center">
      <div class="fw-bold fs-5" id="appModalTitle">Aviso</div>
      <button class="btn btn-outline-secondary btn-sm" id="appModalClose"><i class="bi bi-x-lg"></i></button>
    </div>
    <div class="p-3" id="appModalBody"></div>
    <div class="p-3 border-top text-end" id="appModalFooter"><button class="btn btn-primary fw-bold" id="appModalOk">Aceptar</button></div>
  </div>`;
  document.body.appendChild(div);
  $('appModalClose').addEventListener('click',()=>closeAppModal());
}
function scannerFocusIsAllowed(){
  if(loadInputMode !== 'scanner' || !selected || scannerBusy) return false;
  const fs=document.getElementById('floatingScanner');
  if(fs && fs.classList.contains('show')) return true;
  const normal=$('scannerInput');
  return !!(normal && !normal.disabled);
}
function restoreScannerFocus(delay=120){
  if(!scannerFocusIsAllowed()) return;
  window.setTimeout(()=>{
    if(!scannerFocusIsAllowed()) return;
    const input=currentScannerInput();
    if(input && !input.disabled){
      input.focus({preventScroll:true});
      try{ input.select(); }catch(e){}
    }
  }, delay);
}
function closeAppModal(){ const m=$('appModal'); if(m) m.style.display='none'; restoreScannerFocus(150); }
function appAlert(message,title='Aviso'){
  ensureAppModal();
  $('appModalTitle').textContent=title;
  $('appModalBody').innerHTML=String(message??'');
  $('appModalFooter').innerHTML=`<button class="btn btn-primary fw-bold" id="appModalOk">Aceptar</button>`;
  $('appModalOk').addEventListener('click',closeAppModal);
  $('appModal').style.display='flex';
}
function appConfirm(message,title='Confirmar',confirmText='Aceptar',onConfirm=null){
  ensureAppModal();
  $('appModalTitle').textContent=title;
  $('appModalBody').innerHTML=String(message??'');
  $('appModalFooter').innerHTML=`<button class="btn btn-outline-secondary fw-bold me-2" id="appModalCancel">Cancelar</button><button class="btn btn-primary fw-bold" id="appModalConfirm">${h(confirmText)}</button>`;
  $('appModalCancel').addEventListener('click',closeAppModal);
  $('appModalConfirm').addEventListener('click',async()=>{
    const cb=onConfirm;
    closeAppModal();
    if(cb) await cb();
    restoreScannerFocus(220);
  });
  $('appModal').style.display='flex';
}
window.alert=(m)=>appAlert(h(m));

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 statusBadge(s){
  s=String(s||'').toUpperCase();
  let cls=s==='COMPLETA'?'success':s==='EN_PROCESO'?'info':'warning';
  let label=s==='COMPLETA'?'LISTA PARA SURTIR':(s==='EN_PROCESO'?'CARGANDO UNIDAD':'PENDIENTE DE CARGA');
  return `<span class="badge text-bg-${cls} badge-status">${h(label)}</span>`;
}
function groupKey(it){
  const oc=String(it.purchase_order||'SIN OC').trim() || 'SIN OC';
  const prod=String(it.product_id||'0');
  const label=String(it.product_label||'SIN ARTÍCULO').trim() || 'SIN ARTÍCULO';
  return `${oc}||${prod}||${label}`;
}
function buildGroups(items){
  const map={};
  items.forEach(it=>{
    const key=groupKey(it);
    if(!map[key]){
      map[key]={
        key,
        purchase_order:String(it.purchase_order||'SIN OC').trim() || 'SIN OC',
        product_id:n(it.product_id),
        product_label:String(it.product_label||'SIN ARTÍCULO').trim() || 'SIN ARTÍCULO',
        total:0,
        loaded:0,
        pallets:0,
        packages:0,
        pieces:0,
        slots:[],
        min_slot:999999
      };
    }
    map[key].total++;
    map[key].loaded += Number(it.loaded)?1:0;
    map[key].pallets += n(it.pallets_qty);
    map[key].packages += n(it.packages_qty);
    map[key].pieces += n(it.pieces_qty);
    map[key].slots.push(n(it.slot_no));
    map[key].min_slot = Math.min(map[key].min_slot,n(it.slot_no));
  });
  return Object.values(map).sort((a,b)=>a.min_slot-b.min_slot);
}
function currentGroupKey(groups){
  const g=groups.find(x=>x.loaded<x.total);
  return g ? g.key : '';
}
function renderGroupSummary(groups,currentKey){
  const done=groups.filter(g=>g.loaded>=g.total).length;
  const total=groups.length;
  const current=groups.find(g=>g.key===currentKey);
  return `<div class="row g-2 mb-3">
    <div class="col-md-4"><div class="p-3 rounded-4 bg-light"><div class="small muted">Avance por OC / artículo</div><b>${done}/${total}</b> grupos terminados</div></div>
    <div class="col-md-4"><div class="p-3 rounded-4 bg-light"><div class="small muted">Grupo actual</div><b>${current?h(current.purchase_order):'Finalizado'}</b><div class="small muted">${current?h(current.product_label):'Todas las cargas completadas'}</div></div></div>
    <div class="col-md-4"><div class="p-3 rounded-4 bg-light"><div class="small muted">Tarimas cargadas</div><b>${groups.reduce((a,g)=>a+g.loaded,0)}/${groups.reduce((a,g)=>a+g.total,0)}</b></div></div>
  </div>
  <div class="d-grid gap-2 mb-3">
    ${groups.map((g,idx)=>{
      const complete=g.loaded>=g.total;
      const current=g.key===currentKey;
      const cls=complete?'done':(current?'current':'pending');
      const pill=complete?'counter-done':(current?'counter-current':'counter-pending');
      const label=complete?'Terminado':(current?'Cargando ahora':'Pendiente');
      return `<div class="group-card ${cls}">
        <div class="d-flex justify-content-between flex-wrap gap-2">
          <div><div class="fw-bold">#${idx+1} · OC ${h(g.purchase_order)}</div><div class="small muted">${h(g.product_label)}</div></div>
          <div class="text-end"><span class="counter-pill ${pill}">${label}</span><div class="small mono mt-1">${g.loaded}/${g.total} tarimas · ${fmt(g.packages)} paquetes</div></div>
        </div>
      </div>`;
    }).join('')}
  </div>`;
}

function itemCanToggle(it,currentKey,complete){
  if(complete) return false;
  if(Number(it.loaded)) return true;
  return groupKey(it)===currentKey;
}
async function loadOrders(page=currentPage){
  const filterDate=$('filterDate')?.value || '';
  currentPage=Math.max(1,Number(page||1));
  const j=await apiGet('orders',{date:filterDate,page:currentPage,per_page:20});
  if(!j.ok){appAlert(h(j.error||'Error'));return;}
  rows=j.rows||[];
  pageMeta=j.meta||{date:filterDate,page:currentPage,pages:1,total:rows.length,per_page:20};
  currentPage=Number(pageMeta.page||1);
  const totalOrders=Number(pageMeta.total||rows.length);
  const totalSlots=rows.reduce((a,r)=>a+n(r.total_slots),0);
  const loadedSlots=rows.reduce((a,r)=>a+n(r.loaded_slots),0);
  const totalPallets=rows.reduce((a,r)=>a+n(r.total_pallets),0);
  const pctAll=totalSlots>0?Math.round((loadedSlots/totalSlots)*100):0;
  if($('kOrders')) $('kOrders').textContent=fmt(totalOrders);
  if($('kPallets')) $('kPallets').textContent=fmt(totalPallets);
  if($('kProgress')) $('kProgress').textContent=pctAll+'%';
  if($('kNext')) $('kNext').textContent=totalOrders>0?'Atender orden':'Sin pendientes';
  $('ordersList').innerHTML=rows.length?rows.map(r=>{
    const total=n(r.total_slots), loaded=n(r.loaded_slots);
    const pct=total>0?Math.round((loaded/total)*100):0;
    const status=String(r.status||'').toUpperCase();
    const cardCls=status==='COMPLETA'?'border-success bg-success-subtle':(loaded>0?'border-primary bg-primary-subtle':'');
    const userName=String(r.confirmed_by_name||r.load_started_by_name||'').trim();
    const userLabel=userName ? `<div class="small text-primary"><i class="bi bi-person-check me-1"></i>${h(userName)}</div>` : `<div class="small muted"><i class="bi bi-person me-1"></i>Sin montacarguista asignado</div>`;
    return `<div class="order-card ${cardCls}" data-id="${r.id}">
      <div class="d-flex justify-content-between"><b class="mono">#${r.id} · ${h(r.sales_order_ref||'')}</b>${statusBadge(r.status)}</div>
      <div>${h(r.business_name||r.contact_name||'')}</div>
      <div class="small muted">Unidad ${h(r.vehicle_unit||'')}</div>
      ${userLabel}
      <div class="small muted">Fecha entrega: <b>${h(r.requested_delivery_date||'')}</b></div>
      <div class="small muted">Tarimas: <b>${fmt(r.total_pallets)}</b> · Cargadas: <b>${fmt(loaded)}/${fmt(total)}</b></div>
      <div class="progress mt-2" style="height:8px"><div class="progress-bar ${pct===100?'bg-success':(pct>0?'bg-primary':'bg-secondary')}" style="width:${pct}%"></div></div>
    </div>`;
  }).join(''):'<div class="text-center muted py-4">Sin órdenes de carga para la fecha seleccionada.</div>';
  renderPager();
  document.querySelectorAll('.order-card').forEach(el=>el.addEventListener('click',()=>openDetail(el.dataset.id)));
}

function renderPager(){
  const meta=pageMeta||{};
  const total=Number(meta.total||0), page=Number(meta.page||1), pages=Number(meta.pages||1);
  const box=$('ordersPager');
  if(!box) return;
  if(total<=0){ box.innerHTML=''; return; }
  box.innerHTML=`<div class="small muted">Mostrando página <b>${page}</b> de <b>${pages}</b> · ${fmt(total)} orden(es)</div>
    <div class="btn-group">
      <button class="btn btn-outline-primary btn-sm" ${page<=1?'disabled':''} onclick="loadOrders(${page-1})"><i class="bi bi-chevron-left"></i></button>
      <button class="btn btn-outline-primary btn-sm" ${page>=pages?'disabled':''} onclick="loadOrders(${page+1})"><i class="bi bi-chevron-right"></i></button>
    </div>`;
}

function setLoadInputMode(mode){
  loadInputMode = mode === 'scanner' ? 'scanner' : 'manual';
  document.querySelectorAll('[data-mode-btn]').forEach(btn=>btn.classList.toggle('active',btn.dataset.modeBtn===loadInputMode));
  const panel=$('scannerPanel');
  if(panel) panel.classList.toggle('scanner-active',loadInputMode==='scanner');
  document.querySelectorAll('.manual-load-switch').forEach(el=>{ el.disabled = (loadInputMode==='scanner') || el.dataset.locked==='1'; });
  const input=$('scannerInput');
  if(input){ input.disabled = loadInputMode!=='scanner' || !selected; }
  const expected=$('scannerExpected')?.textContent || 'Selecciona una orden.';
  setScannerExpectedText(expected);
  showFloatingScanner(loadInputMode==='scanner');
}
function currentScannerExpected(groups,curKey){
  const g=groups.find(x=>x.key===curKey);
  if(!g) return 'Todas las tarimas están cargadas.';
  return `Esperando tarima: OC ${g.purchase_order} · ${g.product_label}`;
}
function ensureFloatingScanner(){
  if(document.getElementById('floatingScanner')) return;
  const div=document.createElement('div');
  div.id='floatingScanner';
  div.className='floating-scanner';
  div.innerHTML=`<div class="floating-scanner-card">
    <div class="floating-scanner-head">
      <div class="min-w-0">
        <div class="floating-scanner-title"><i class="bi bi-qr-code-scan me-1"></i>Lectura flotante de tarima</div>
        <div class="floating-scanner-expected" id="floatingScannerExpected">Selecciona una orden.</div>
      </div>
      <button type="button" class="btn btn-outline-secondary btn-sm" id="floatingScannerClose"><i class="bi bi-x-lg"></i></button>
    </div>
    <div class="d-flex gap-2 flex-wrap">
      <input id="floatingScannerInput" class="form-control floating-scanner-input flex-grow-1" autocomplete="off" inputmode="text" placeholder="Escanea el QR de la tarima" onkeydown="if(event.key==='Enter'){event.preventDefault();submitScannerRead();}">
      <button type="button" class="btn btn-success fw-bold" id="floatingScannerSubmit"><i class="bi bi-check2-circle me-1"></i><span class="scan-action-text">Validar</span></button>
      <button type="button" class="btn btn-camera" id="floatingScannerCamera"><i class="bi bi-camera-video me-1"></i><span class="scan-action-text">Cámara</span></button>
    </div>
    <div class="scan-hint mt-2">El sistema respeta el orden por OC + artículo, asigna la lectura al siguiente espacio pendiente y bloquea etiquetas repetidas.</div>
  </div>`;
  document.body.appendChild(div);
  $('floatingScannerClose').addEventListener('click',()=>setLoadInputMode('manual'));
  $('floatingScannerSubmit').addEventListener('click',()=>submitScannerRead());
  $('floatingScannerCamera').addEventListener('click',()=>startCameraScanner());
}
function setScannerExpectedText(text){
  const ex=$('floatingScannerExpected');
  if(ex) ex.textContent=text || 'Selecciona una orden.';
}
function showFloatingScanner(show=true){
  ensureFloatingScanner();
  const fs=$('floatingScanner');
  if(!fs) return;
  fs.classList.toggle('show', !!show && !!selected);
  const input=$('floatingScannerInput');
  if(input){ input.disabled = !(show && selected); if(show && selected) setTimeout(()=>input.focus(),100); }
}
function clearScannerInputs(){
  const a=$('scannerInput'), b=$('floatingScannerInput');
  if(a) a.value='';
  if(b) b.value='';
}
function currentScannerInput(){
  const floating=$('floatingScannerInput');
  const normal=$('scannerInput');
  if(floating && document.getElementById('floatingScanner')?.classList.contains('show')) return floating;
  return normal || floating;
}
async function submitScannerRead(){
  if(scannerBusy || !selected) return;
  const input=currentScannerInput();
  const code=String(input?.value||'').trim();
  if(!code) return;
  const now=Date.now();
  lastScannerCode=code; lastScannerAt=now;
  scannerBusy=true;
  document.querySelectorAll('#scannerInput,#floatingScannerInput,#floatingScannerSubmit,#floatingScannerCamera').forEach(el=>{ if(el) el.disabled=true; });
  const j=await apiPost('scan_label',{id:selected.id,label_code:code});
  scannerBusy=false;
  document.querySelectorAll('#scannerInput,#floatingScannerInput,#floatingScannerSubmit,#floatingScannerCamera').forEach(el=>{ if(el) el.disabled=false; });
  clearScannerInputs();
  if(!j.ok){
    const msg=String(j.error||'El código escaneado no corresponde al grupo actual.');
    const isDup=/DUPLICADA|ya fue escaneada|ya fue utilizada/i.test(msg);
    const title=isDup?'Etiqueta ya utilizada':'Lectura no permitida';
    appAlert(`<div class="scan-error-strong"><b>${h(title)}</b><br>${h(msg)}</div><div class="small muted mt-2">La tarima no fue marcada como cargada. Respeta el orden sugerido por OC y artículo.</div>`,'Alerta de escaneo');
    restoreScannerFocus(220);
    return;
  }
  const scan=j.scan||{};
  if(scan.duplicate){
    handleDuplicateScan(scan);
    return;
  }
  await openDetail(selected.id, `${scan.purchase_order||''}||${scan.product_id||''}||${scan.product_label||''}`);
  await loadOrders(currentPage);
  restoreScannerFocus(220);
}


function renderDuplicateRelease(scan){
  const label=scan?.label_code||'';
  return `<div class="alert alert-warning mb-3"><b>Etiqueta ya escaneada</b><br>${h(scan?.message||'Esta etiqueta ya fue utilizada en una lectura anterior.')}</div>
    <div class="p-2 rounded-3 bg-light mb-2"><div class="small muted">Etiqueta</div><b class="mono">${h(label)}</b></div>
    <div class="small muted">Si fue una lectura incorrecta, puedes liberar la etiqueta para que la tarima vuelva a quedar disponible y pueda escanearse nuevamente.</div>`;
}
async function releaseScannedLabel(scan){
  if(!selected) return;
  const j=await apiPost('release_scanned_label',{id:selected.id,label_code:scan?.label_code||'',item_id:scan?.duplicate_item_id||0});
  if(!j.ok){ appAlert(h(j.error||'No se pudo liberar la etiqueta.'),'Liberar etiqueta'); restoreScannerFocus(260); return; }
  const r=j.released||{};
  appAlert(`<div class="alert alert-success mb-2"><b>Etiqueta liberada</b><br>La tarima quedó disponible para volver a escanear.</div><div class="small"><b>OC:</b> ${h(r.purchase_order||'SIN OC')} · <b>Espacio:</b> ${h(r.slot_no||'')}</div><div class="small mono mt-1">${h(r.label_code||'')}</div>`,'Liberación correcta');
  await openDetail(selected.id);
  await loadOrders(currentPage);
  restoreScannerFocus(350);
}
function handleDuplicateScan(scan){
  appConfirm(renderDuplicateRelease(scan),'Etiqueta ya utilizada','Liberar etiqueta',async()=>releaseScannedLabel(scan));
  restoreScannerFocus(300);
}
function renderPiecesShortage(check){
  const rows=(check?.missing_rows||[]);
  const allRows=(check?.rows||[]);
  const fmt0=v=>Number(v||0).toLocaleString('es-MX',{maximumFractionDigits:0});
  const detail=(rows.length?rows:allRows).map(r=>`<div class="summary-row">
    <div class="fw-bold">OC ${h(r.purchase_order||'SIN OC')} · Espacio ${h(r.slot_no||'')}</div>
    <div>${h(r.product_label||'')}</div>
    <div class="small muted">Etiqueta: <span class="mono">${h(r.label_code||'SIN ETIQUETA')}</span></div>
    <div class="small"><b>Requeridas:</b> ${fmt0(r.expected_pieces)} · <b>Escaneadas:</b> ${fmt0(r.scanned_pieces)} · <b>Faltan:</b> ${fmt0(r.missing_pieces)}</div>
  </div>`).join('');
  return `<div class="alert alert-danger mb-3"><b>No se puede finalizar la carga.</b><br>Las tarimas escaneadas no completan las piezas requeridas por la OC.</div>
    <div class="row g-2 mb-3">
      <div class="col-4"><div class="p-2 rounded-3 bg-light"><div class="small muted">Requeridas</div><b>${fmt0(check?.expected_pieces)}</b></div></div>
      <div class="col-4"><div class="p-2 rounded-3 bg-light"><div class="small muted">Escaneadas</div><b>${fmt0(check?.scanned_pieces)}</b></div></div>
      <div class="col-4"><div class="p-2 rounded-3 bg-light"><div class="small muted">Faltan</div><b class="text-danger">${fmt0(check?.missing_pieces)}</b></div></div>
    </div>
    <div class="summary-list">${detail}</div>`;
}

function ensureCameraScannerModal(){
  if(document.getElementById('cameraScannerModal')) return;
  const div=document.createElement('div');
  div.id='cameraScannerModal';
  div.className='app-modal-backdrop';
  div.innerHTML=`<div class="app-modal-card" style="width:min(720px,100%)">
    <div class="p-3 border-bottom d-flex justify-content-between align-items-center gap-2">
      <div>
        <div class="fw-bold fs-5"><i class="bi bi-camera-video me-2 text-primary"></i>Escanear con cámara</div>
        <div class="small muted">Apunta al QR o código de tarima. La lectura se validará contra la orden actual.</div>
      </div>
      <button class="btn btn-outline-secondary btn-sm" id="cameraScannerClose"><i class="bi bi-x-lg"></i></button>
    </div>
    <div class="p-3">
      <div class="camera-scan-video-wrap">
        <video id="cameraScannerVideo" class="camera-scan-video" playsinline muted></video>
        <div class="camera-scan-frame"></div>
        <div class="camera-scan-line"></div>
      </div>
      <div class="camera-scan-card">
        <div class="camera-status" id="cameraScannerStatus">Preparando cámara...</div>
        <div class="camera-read-code mt-2 d-none" id="cameraReadCode"></div>
      </div>
    </div>
    <div class="p-3 border-top d-flex justify-content-between gap-2 flex-wrap">
      <button class="btn btn-outline-secondary" id="cameraScannerCancel"><i class="bi bi-x-circle me-1"></i>Cerrar cámara</button>
      <button class="btn btn-outline-primary" id="cameraScannerManual"><i class="bi bi-keyboard me-1"></i>Usar captura manual</button>
    </div>
  </div>`;
  document.body.appendChild(div);
  $('cameraScannerClose').addEventListener('click',stopCameraScanner);
  $('cameraScannerCancel').addEventListener('click',stopCameraScanner);
  $('cameraScannerManual').addEventListener('click',()=>{ stopCameraScanner(); setLoadInputMode('scanner'); setTimeout(()=>{$('scannerInput')?.focus();},120); });
}
function cameraSupportedMessage(){
  if(!window.isSecureContext) return 'La cámara requiere HTTPS o servidor local seguro. Abre el sistema con https:// para permitir el acceso a cámara.';
  if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) return 'Este navegador no permite usar la cámara desde la página. Usa el escáner físico o captura manual.';
  if(!('BarcodeDetector' in window)) return 'Este navegador no trae lector nativo de códigos. Usa Chrome/Edge Android actualizado, escáner físico o captura manual.';
  return '';
}
async function startCameraScanner(){
  if(!selected){ appAlert('Selecciona una orden antes de escanear con cámara.','Cámara'); return; }
  const supportMsg=cameraSupportedMessage();
  if(supportMsg){
    appAlert(`<div class="alert alert-warning mb-2"><b>No se pudo iniciar lector por cámara</b></div><div>${h(supportMsg)}</div><div class="small muted mt-2">El modo Escáner físico sigue disponible porque escribe el código en el campo de lectura.</div>`,'Cámara no disponible');
    return;
  }
  ensureCameraScannerModal();
  $('cameraScannerModal').style.display='flex';
  $('cameraScannerStatus').textContent='Solicitando permiso de cámara...';
  $('cameraReadCode').classList.add('d-none');
  cameraLastCode=''; cameraLastAt=0;
  try{
    const formats=['qr_code','code_128','code_39','ean_13','ean_8','upc_a','upc_e','data_matrix'];
    cameraDetector=new BarcodeDetector({formats});
    cameraStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:'environment'},width:{ideal:1280},height:{ideal:720}},audio:false});
    const video=$('cameraScannerVideo');
    video.srcObject=cameraStream;
    await video.play();
    cameraLoopActive=true;
    $('cameraScannerStatus').textContent='Cámara activa. Centra el código dentro del recuadro.';
    requestAnimationFrame(cameraScanLoop);
  }catch(e){
    stopCameraScanner(false);
    appAlert(`<div class="alert alert-danger mb-2"><b>No se pudo abrir la cámara</b></div><div>${h(e && e.message ? e.message : 'Permiso denegado o cámara no disponible.')}</div>`,'Cámara');
  }
}
async function cameraScanLoop(){
  if(!cameraLoopActive || !cameraDetector) return;
  const video=$('cameraScannerVideo');
  try{
    if(video && video.readyState>=2){
      const codes=await cameraDetector.detect(video);
      if(codes && codes.length){
        const raw=String(codes[0].rawValue||'').trim();
        const now=Date.now();
        if(raw && (raw!==cameraLastCode || now-cameraLastAt>1800)){
          cameraLastCode=raw; cameraLastAt=now;
          $('cameraReadCode').textContent=raw;
          $('cameraReadCode').classList.remove('d-none');
          $('cameraScannerStatus').textContent='Código detectado. Validando contra la orden...';
          await submitCameraCode(raw);
          return;
        }
      }
    }
  }catch(e){
    // Continúa intentando; algunos frames pueden fallar si la cámara cambia enfoque.
  }
  if(cameraLoopActive) setTimeout(()=>requestAnimationFrame(cameraScanLoop),160);
}
async function submitCameraCode(code){
  if(scannerBusy || !selected) return;
  scannerBusy=true;
  const j=await apiPost('scan_label',{id:selected.id,label_code:code});
  scannerBusy=false;
  if(!j.ok){
    $('cameraScannerStatus').innerHTML=`<span class="text-danger fw-bold">Lectura no permitida:</span> ${h(j.error||'Código incorrecto.')}`;
    setTimeout(()=>{ if(cameraLoopActive){ $('cameraScannerStatus').textContent='Intenta con otra etiqueta o centra nuevamente el código.'; requestAnimationFrame(cameraScanLoop); } },1800);
    return;
  }
  const scan=j.scan||{};
  if(scan.duplicate){
    $('cameraScannerStatus').innerHTML=`<span class="text-warning fw-bold">Etiqueta ya escaneada.</span> Revisa la opción para liberar.`;
    handleDuplicateScan(scan);
    setTimeout(()=>{ if(cameraLoopActive){ requestAnimationFrame(cameraScanLoop); } },500);
    return;
  }
  $('cameraScannerStatus').innerHTML=`<span class="text-success fw-bold">Lectura aceptada:</span> espacio ${h(scan.slot_no||'')} · ${h(scan.product_label||'')}`;
  stopCameraStreamOnly();
  await openDetail(selected.id, `${scan.purchase_order||''}||${scan.product_id||''}||${scan.product_label||''}`);
  await loadOrders(currentPage);
  setTimeout(()=>{ if($('cameraScannerModal')) $('cameraScannerModal').style.display='none'; },900);
}
function stopCameraStreamOnly(){
  cameraLoopActive=false;
  if(cameraStream){ cameraStream.getTracks().forEach(t=>{try{t.stop();}catch(e){}}); cameraStream=null; }
  const video=$('cameraScannerVideo');
  if(video) video.srcObject=null;
}
function stopCameraScanner(hide=true){
  stopCameraStreamOnly();
  if(hide && $('cameraScannerModal')) $('cameraScannerModal').style.display='none';
}

async function openDetail(id, noticeKey=''){
  const j=await apiGet('detail',{id});
  if(!j.ok){appAlert(h(j.error||'Error'));return;}
  selected=j.order;
  const items=j.items||[];
  const groups=buildGroups(items);
  const curKey=currentGroupKey(groups);
  document.querySelectorAll('.order-card').forEach(e=>e.classList.toggle('active',String(e.dataset.id)===String(id)));
  const complete=String(selected.status).toUpperCase()==='COMPLETA';
  const allLoaded=groups.length>0 && groups.every(g=>g.loaded>=g.total);
  if(noticeKey){
    const g=groups.find(x=>x.key===noticeKey);
    if(g && g.loaded>=g.total && lastCompletedNoticeKey!==noticeKey){
      lastCompletedNoticeKey=noticeKey;
      const next=groups.find(x=>x.loaded<x.total);
      appAlert(
        `<div class="alert alert-success mb-2"><b>Has finalizado la carga de la OC ${h(g.purchase_order)}</b><br>${h(g.product_label)}</div>`+
        (next?`Puedes continuar con la siguiente carga:<br><b>OC ${h(next.purchase_order)}</b><br>${h(next.product_label)}`:'Todas las OC/artículos de esta orden quedaron cargados. Ya puedes confirmar carga completa.'),
        'Carga terminada'
      );
    }
  }
  $('detailBox').className='detail-loaded';
  const started=String(selected.status||'').toUpperCase()!=='PENDIENTE' || !!selected.load_started_at;
  const startInfo=selected.load_started_at?`<span class="time-pill"><i class="bi bi-clock-history"></i>Inicio: ${h(fmtDateTime(selected.load_started_at))}</span><span class="time-pill"><i class="bi bi-stopwatch"></i>Tiempo: ${h(elapsedFrom(selected.load_started_at))}</span>`:`<span class="time-pill"><i class="bi bi-hourglass-split"></i>Sin iniciar</span>`;
  $('detailBox').innerHTML=`<div class="detail-top">
    <div class="detail-title">
      <div class="small muted">Orden de carga</div>
      <h4 class="m-0 mono">#${h(selected.id)} · ${h(selected.sales_order_ref||'')}</h4>
    </div>
    <div class="d-flex gap-2 align-items-start flex-wrap justify-content-end">
      <button type="button" class="btn btn-outline-primary load-info-btn" onclick="showLoadInfoModal()"><i class="bi bi-info-circle me-1"></i>Información de la carga</button>
      ${statusBadge(selected.status)}
    </div>
  </div>
  <div class="load-control-card ${started?'started':''}">
    <div><div class="fw-bold"><i class="bi bi-truck-front me-1"></i>${started?'Carga iniciada':'Carga sin iniciar'}</div><div class="mt-2 d-flex gap-2 flex-wrap">${startInfo}</div></div>
    <button class="btn btn-primary" onclick="confirmStartLoad(${selected.id})" ${complete||started?'disabled':''}><i class="bi bi-play-circle me-1"></i>Iniciar carga</button>
  </div>
  ${renderGroupSummary(groups,curKey)}
  <div class="scanner-panel ${loadInputMode==='scanner'?'scanner-active':''}" id="scannerPanel">
    <div class="d-flex flex-wrap align-items-end justify-content-between gap-2">
      <div>
        <div class="fw-bold"><i class="bi bi-upc-scan me-1"></i>Modo de carga</div>
        <div class="scan-hint" id="scannerExpected">${h(currentScannerExpected(groups,curKey))}</div>
      </div>
      <div class="btn-group mode-toggle" role="group">
        <button type="button" class="btn btn-outline-primary ${loadInputMode==='manual'?'active':''}" data-mode-btn="manual" onclick="setLoadInputMode('manual')"><i class="bi bi-hand-index-thumb me-1"></i>Manual</button>
        <button type="button" class="btn btn-outline-success ${loadInputMode==='scanner'?'active':''}" data-mode-btn="scanner" onclick="setLoadInputMode('scanner')"><i class="bi bi-upc-scan me-1"></i>Escáner</button>
      </div>
    </div>
    <div class="mt-2">
      <label class="form-label fw-bold mb-1">Lectura de tarima / QR</label>
      <div class="d-flex gap-2 flex-wrap">
        <input id="scannerInput" class="form-control scanner-input flex-grow-1" autocomplete="off" placeholder="Modo escáner activo: usa el campo flotante inferior" ${loadInputMode==='scanner'&&!complete?'':'disabled'} onkeydown="if(event.key==='Enter'){event.preventDefault();submitScannerRead();}">
        <button type="button" class="btn btn-camera" onclick="startCameraScanner()" ${complete?'disabled':''}><i class="bi bi-camera-video me-1"></i>Cámara</button>
      </div>
      <div class="scan-hint mt-1">Puedes usar escáner físico, captura manual o cámara del dispositivo. El sistema valida contra BD: etiqueta de producción si existe, artículo actual por OC y evita lecturas duplicadas.</div>
    </div>
  </div>
  <div class="mobile-current-banner"><i class="bi bi-lightning-charge-fill me-1"></i> Sigue el flujo por OC y artículo. Solo se habilita la carga actual.</div>
  <div class="truck-stage compact-only">
    <div class="truck-bed"><div class="truck-direction"><span>INTERIOR</span><span class="line"></span><i class="bi bi-arrow-right"></i><span>EXTERIOR</span></div><div class="slots-board">${items.map(it=>{
    const key=groupKey(it);
    const g=groups.find(x=>x.key===key);
    const can=itemCanToggle(it,curKey,complete);
    const loaded=Number(it.loaded);
    const cardCls=loaded?'loaded group-done':(key===curKey?'current':'blocked');
    const disabled=can?'':'disabled';
    const badge=loaded?'<span class="slot-badge done"><i class="bi bi-check2-circle"></i>Cargado</span>':(key===curKey?'<span class="slot-badge current"><i class="bi bi-play-fill"></i>Actual</span>':'<span class="slot-badge locked"><i class="bi bi-lock"></i>En espera</span>');
    return `<div class="slot-card ${cardCls}">
        <div class="slot-title-row">
          <div><div class="fw-black"><b>Espacio ${h(it.slot_no)}</b></div>${badge}</div>
          <div class="form-check form-switch m-0"><input class="form-check-input manual-load-switch" type="checkbox" data-locked="${can?'0':'1'}" ${loaded?'checked':''} ${disabled} onchange="toggleItem(${it.id},this.checked,'${encodeURIComponent(key)}')"></div>
        </div>
        <div class="mono small mt-2"><b>OC:</b> ${h(it.purchase_order||'')}</div>
        <div class="fw-semibold mt-1">${h(it.customer_label||'')}</div>
        <div class="small muted">${h(it.delivery_address||'')}</div>
        <div class="slot-product">${h(it.product_label||'')}</div>
        <div class="small mono mt-1">${fmt(it.packages_qty)} paquetes · ${fmt(it.pieces_qty)} piezas</div>
        ${it.loaded_by_name?`<div class="small text-success mt-1"><i class="bi bi-person-check me-1"></i>Cargó: ${h(it.loaded_by_name)} · ${h(fmtDateTime(it.loaded_at))}</div>`:''}
        ${it.scanned_label_code?`<div class="small text-primary mt-1"><i class="bi bi-upc-scan me-1"></i>${h(it.scanned_label_code)}</div>`:''}
        ${!can&&!complete?'<div class="small text-secondary mt-2"><i class="bi bi-lock me-1"></i>Pendiente hasta finalizar el grupo anterior.</div>':''}
      </div>`;
  }).join('')}</div></div></div>
  <div class="action-bar">
    <button class="btn btn-success" onclick="confirmCompleteOrder(${selected.id})" ${complete||!allLoaded?'disabled':''}><i class="bi bi-check2-circle me-1"></i>Confirmar carga completa</button>
  </div>`;
  fillLoadInfoModal(groups,curKey,items);
  setLoadInputMode(complete ? 'manual' : loadInputMode);
  setScannerExpectedText(currentScannerExpected(groups,curKey));
}
function confirmStartLoad(id){
  appConfirm(`<div class="alert alert-primary mb-2"><b>¿Iniciar carga de esta orden?</b></div><div class="small muted">Se registrará la hora actual como inicio de carga. A partir de aquí podrás medir cuánto tardó la unidad.</div>`, 'Iniciar carga', 'Iniciar carga', async()=>setProcess(id));
}
async function setProcess(id){
  const j=await apiPost('set_process',{id});
  if(!j.ok){appAlert(h(j.error||'No se pudo iniciar'));return;}
  await loadOrders();
  await openDetail(id);
}
async function toggleItem(item_id,loaded,encodedKey=''){
  const key=decodeURIComponent(encodedKey||'');
  const j=await apiPost('toggle_item',{item_id,loaded:loaded?1:0});
  if(!j.ok){appAlert(h(j.error||'No se pudo actualizar'));return;}
  if(selected) await openDetail(selected.id, loaded?key:'');
  await loadOrders();
}
function buildLocalCompleteSummary(){
  const order=selected||{};
  const slotCards=[...document.querySelectorAll('.slot-card')];
  const totalSlots=slotCards.length;
  return `<div class="alert alert-success mb-3"><b>¿Confirmar finalización de carga?</b><br>Se guardará la hora final y el pedido quedará listo para surtir desde Almacén.</div>
    <div class="row g-2 mb-3">
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Orden</div><b>#${h(order.id||'')}</b></div></div>
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Inicio</div><b>${h(fmtDateTime(order.load_started_at))}</b></div></div>
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Tiempo transcurrido</div><b>${h(elapsedFrom(order.load_started_at)||'—')}</b></div></div>
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Espacios cargados</div><b>${fmt(totalSlots)}</b></div></div>
    </div>
    <label class="form-label fw-bold">Notas de cierre opcionales</label><textarea id="completeNotes" class="form-control" rows="3" maxlength="255" placeholder="Ej. Carga completa sin novedad"></textarea>`;
}
function confirmCompleteOrder(id){
  appConfirm(buildLocalCompleteSummary(), 'Finalizar carga', 'Confirmar finalización', async()=>completeOrder(id, ($('completeNotes')?.value||'')));
}
function renderServerSummary(summary){
  const totals=summary?.totals||{};
  const groups=summary?.groups||[];
  return `<div class="alert alert-success"><b>Carga completa confirmada.</b><br>El pedido quedó listo para surtir en Almacén.</div>
    <div class="row g-2 mb-3">
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Inicio</div><b>${h(fmtDateTime(summary?.started_at))}</b></div></div>
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Fin</div><b>${h(fmtDateTime(summary?.completed_at))}</b></div></div>
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Montacarguista</div><b>${h(summary?.order?.confirmed_by_name||CURRENT_USER||'—')}</b></div></div>
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Duración</div><b>${h(summary?.duration_label||'—')}</b></div></div>
      <div class="col-6"><div class="p-2 rounded-3 bg-light"><div class="small muted">Total</div><b>${fmt(totals.pallets)} tarimas · ${fmt(totals.pieces)} piezas</b></div></div>
    </div>
    <div class="summary-list">${groups.map(g=>`<div class="summary-row"><div class="fw-bold">OC ${h(g.purchase_order||'SIN OC')}</div><div>${h(g.product_label||'')}</div><div class="small muted">${fmt(g.pallets)} tarimas · ${fmt(g.packages)} paquetes · ${fmt(g.pieces)} piezas</div><div class="small muted">${h(g.customer_label||'')}</div></div>`).join('')}</div>`;
}
async function completeOrder(id,notes=''){
  const j=await apiPost('complete',{id,notes});
  if(!j.ok){
    if(j.error_code==='PIECES_SHORTAGE' || j.pieces_check){
      appAlert(renderPiecesShortage(j.pieces_check||{}),'Faltan piezas');
      restoreScannerFocus(300);
      return;
    }
    appAlert(h(j.error||'No se pudo completar'));
    restoreScannerFocus(300);
    return;
  }
  appAlert(renderServerSummary(j.summary||{}),'Resumen de carga');
  await loadOrders();
  await openDetail(id);
}

['userInfoModal','kpiTotalsModal','loadInfoModal'].forEach(id=>{
  const el=$(id);
  if(el) el.addEventListener('hidden.bs.modal',()=>restoreScannerFocus(180));
});
document.addEventListener('visibilitychange',()=>{ if(!document.hidden) restoreScannerFocus(250); });
window.addEventListener('focus',()=>restoreScannerFocus(250));
$('btnReload').addEventListener('click',()=>loadOrders(currentPage));
if($('btnReloadTop')) $('btnReloadTop').addEventListener('click',()=>loadOrders(currentPage));
if($('btnFilterDate')) $('btnFilterDate').addEventListener('click',()=>{currentPage=1;loadOrders(1);});
if($('filterDate')) $('filterDate').addEventListener('change',()=>{currentPage=1;loadOrders(1);});
window.addEventListener('beforeunload',()=>stopCameraStreamOnly());
loadOrders(1);
</script></body></html>
