<?php
/**
 * repartidor_botella.php
 * ============================================================
 * BReport · Planta Botella · MÓDULO REPARTIDOR / REPARTO
 * ------------------------------------------------------------
 * Flujo operativo:
 * - El repartidor trabaja sobre UNA ruta asignada.
 * - La ruta es un almacén móvil RUTA-xx.
 * - Inventario oficial: inventory_moves.
 * - Entrega/venta en ruta: OUT desde almacén RUTA-xx.
 * - Solicitud de recarga: queda pendiente para Almacén/Ventas.
 * - Devolución a almacén físico: OUT desde RUTA-xx + IN a ALM01.
 * - El saldo que quede en RUTA-xx permanece como inventario inicial del
 *   siguiente día operativo, respetando corte 07:00 AM.
 * ============================================================
 */

declare(strict_types=1);

const BREPORT_TIMEZONE = 'America/Matamoros';
date_default_timezone_set(BREPORT_TIMEZONE);

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

require_scope();
ensure_session();

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

$scope = scope();
$companyId  = (int)$scope['company_id'];
$locationId = (int)$scope['location_id'];
$action = (string)($_GET['action'] ?? '');

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

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

$scopePlantText = mb_strtoupper(trim($scopeCompanyName . ' ' . $scopeLocationName), 'UTF-8');
if (!str_contains($scopePlantText, 'BOTELLA')) {
    http_response_code(403);
    exit('Esta sección corresponde únicamente a Planta Botella.');
}

$canUseReparto = has_role('admin') || has_role('warehouse') || has_role('almacen') || has_role('reparto') || has_role('repartidor');
if (!$canUseReparto) {
    http_response_code(403);
    exit('Sin permiso para Reparto Botella. Solicita rol reparto/repartidor.');
}

ensure_repartidor_botella_schema($pdo);

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

function breport_configure_db_timezone_reparto(PDO $pdo): void {
    try { $pdo->exec('SET time_zone = ' . $pdo->quote(breport_timezone_offset_reparto())); } catch (Throwable $e) {}
}

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

function botella_delivery_reference_reparto(int $routeNo, int $userId): string {
    $dt = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE));
    return sprintf('REP-R%02d-%s-U%d', $routeNo, $dt->format('Ymd-His'), $userId);
}

function h_ticket_reparto(?string $s): string {
    return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}

function fmt_ticket_qty_reparto(float $v, int $decimals = 0): string {
    if ($decimals <= 0) return number_format($v, 0, '.', ',');
    return number_format($v, $decimals, '.', ',');
}

function print_delivery_ticket_reparto(PDO $pdo, int $companyId, int $locationId, int $outboundId, string $scopeCompanyName, string $scopeLocationName): void {
    if ($outboundId <= 0) {
        http_response_code(400);
        exit('Ticket inválido.');
    }

    $st = $pdo->prepare("SELECT o.*, w.code AS warehouse_code, w.name AS warehouse_name,
            u.full_name AS created_by_name, u.username AS created_by_username
        FROM outbounds o
        LEFT JOIN warehouses w ON w.id = o.warehouse_id
        LEFT JOIN users u ON u.id = o.created_by
        WHERE o.id = ? AND o.company_id = ? AND o.location_id = ?
        LIMIT 1");
    $st->execute([$outboundId, $companyId, $locationId]);
    $out = $st->fetch(PDO::FETCH_ASSOC);
    if (!$out) {
        http_response_code(404);
        exit('No se encontró la nota de entrega.');
    }

    $st = $pdo->prepare("SELECT oi.product_id, oi.qty, p.sku, p.name,
            COALESCE(ppo.pieces_per_package, p.pieces_per_package, 0) AS pieces_per_package,
            COALESCE(ppo.packages_per_pallet, p.packages_per_pallet, 0) AS packages_per_pallet
        FROM outbound_items oi
        JOIN products p ON p.id = oi.product_id
        LEFT JOIN product_packaging_options ppo ON ppo.company_id = p.company_id AND ppo.product_id = p.id AND ppo.active = 1 AND ppo.is_default = 1
        WHERE oi.outbound_id = ?
        ORDER BY p.sku, p.name");
    $st->execute([$outboundId]);
    $items = $st->fetchAll(PDO::FETCH_ASSOC);

    $createdAt = (string)($out['depart_at'] ?: $out['created_at'] ?: breport_now_reparto());
    $dt = new DateTimeImmutable($createdAt, new DateTimeZone(BREPORT_TIMEZONE));
    $seller = trim((string)($out['created_by_name'] ?: $out['driver_name'] ?: $out['created_by_username'] ?: 'Repartidor'));
    $route = trim((string)($out['warehouse_code'] ?: $out['warehouse_name'] ?: 'Ruta'));

    $totalPieces = 0.0;
    $totalPackages = 0.0;
    $totalPallets = 0.0;
    foreach ($items as $it) {
        $qty = (float)$it['qty'];
        $ppp = (int)($it['pieces_per_package'] ?? 0);
        $ppal = (int)($it['packages_per_pallet'] ?? 0);
        $totalPieces += $qty;
        if ($ppp > 0) $totalPackages += $qty / $ppp;
        if ($ppp > 0 && $ppal > 0) $totalPallets += $qty / ($ppp * $ppal);
    }

    header('Content-Type: text/html; charset=utf-8');
    ?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nota de entrega <?=h_ticket_reparto((string)($out['reference'] ?? ''))?></title>
<style>
@page{size:57mm 50mm;margin:1.5mm;}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;background:#fff;color:#111;font-family:Arial,Helvetica,sans-serif;font-size:7.2px;line-height:1.12;}
.ticket{width:54mm;max-width:54mm;margin:0 auto;padding:0;overflow:hidden;}
.center{text-align:center;}.right{text-align:right}.bold{font-weight:800}.muted{color:#555}.line{border-top:1px dashed #111;margin:2mm 0 1.4mm}.title{font-size:12px;font-weight:900;letter-spacing:.1px;line-height:1}.sub{font-size:6.5px;line-height:1.05}.meta{display:grid;grid-template-columns:13mm 1fr;gap:.4mm 1.2mm;margin-top:1.2mm}.meta div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.items{width:100%;border-collapse:collapse;margin-top:1.3mm;table-layout:fixed}.items th{border-bottom:1px solid #111;font-size:6.2px;text-transform:uppercase;padding:.8mm 0}.items td{border-bottom:1px dotted #999;padding:.9mm 0;vertical-align:top}.sku{font-weight:800;font-size:6.8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.name{font-size:6px;color:#444;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.totals{width:100%;border-collapse:collapse;margin-top:1.2mm}.totals td{padding:.4mm 0}.footer{margin-top:1.4mm;text-align:center;font-size:5.8px}.signature{height:5mm;border-bottom:1px solid #111;margin-top:3mm}.screen-actions{display:flex;gap:6px;justify-content:center;margin:10px auto;width:54mm}.screen-actions button{border:1px solid #111;background:#fff;border-radius:6px;padding:7px 8px;font-weight:800;cursor:pointer;font-size:11px}@media print{.screen-actions{display:none}html,body{width:57mm;min-height:50mm;background:#fff}.ticket{width:54mm;max-width:54mm;margin:0 auto}.no-print{display:none!important}}

.confirm-box{display:grid;grid-template-columns:1fr 1fr;gap:10px;background:#f8fbff;border:1px solid #dbeafe;border-radius:16px;padding:12px}.confirm-box span{display:block}.confirm-box strong{display:block;font-size:1rem;color:#08264a}.confirm-total{display:flex;justify-content:space-between;align-items:center;background:#08264a;color:#fff;border-radius:16px;padding:12px 14px;font-weight:900}.confirm-total strong{font-size:1.15rem}@media(max-width:767px){.confirm-box{grid-template-columns:1fr}.confirm-total{position:sticky;bottom:0}}


/* ============================================================
   AJUSTE UX · CABECERA LIMPIA + KPIs EN MODAL
============================================================ */
.appbar-modern{padding:12px 0 16px!important;border-radius:0 0 20px 20px!important;background:linear-gradient(135deg,#061b35,#0f3b63 62%,#186da7)!important;}
.appbar-grid-modern{grid-template-columns:minmax(280px,1fr) minmax(520px,880px)!important;align-items:center!important;gap:16px!important;}
.brand-block{display:grid;gap:3px;}
.brand-eyebrow{font-size:.74rem;opacity:.76;font-weight:750;}
.brand-meta{font-size:.78rem;opacity:.82;}
.route-mini{display:inline-flex;align-items:center;gap:8px;width:max-content;max-width:100%;margin-top:5px;padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.16);backdrop-filter:blur(10px);}
.route-mini-label{font-size:.67rem;opacity:.78;text-transform:uppercase;letter-spacing:.05em;font-weight:900;}
.route-mini strong{font-size:.78rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:420px;}
.toolbar-modern{grid-template-columns:145px minmax(220px,1fr) 96px 126px 74px!important;align-items:center!important;}
.btn-kpi{background:linear-gradient(135deg,#e8f4ff,#fff);border-color:#9ec5fe!important;color:#0f3b63!important;font-weight:950!important;}
.btn-kpi:hover{background:#fff!important;}
.kpi-grid-modal{margin-left:-8px;margin-right:-8px;}
.kpi-grid-modal .cardx{box-shadow:0 10px 24px rgba(15,23,42,.08);}
.kpi-tile{background:linear-gradient(180deg,#f8fbff,#eef6ff);border-color:#bcd7ff;}
@media (min-width:1200px){.action-grid{grid-template-columns:repeat(6,minmax(0,1fr));}.action-tile{min-height:138px}.shell{margin-top:14px!important;}}
@media (min-width:768px) and (max-width:1199px){.appbar-grid-modern{grid-template-columns:1fr!important}.toolbar-modern{grid-template-columns:140px minmax(210px,1fr) 96px 126px 74px!important}.route-mini strong{max-width:620px}.action-grid{grid-template-columns:repeat(3,minmax(0,1fr));}.action-tile:last-child{grid-column:auto;}}
@media (max-width:767px){.appbar-modern{padding:10px 0 12px!important;border-radius:0 0 16px 16px!important}.brand-eyebrow{display:none}.brand-meta{font-size:.72rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.route-mini{width:100%;justify-content:space-between;padding:5px 8px}.route-mini strong{max-width:72%;font-size:.73rem}.toolbar-modern{grid-template-columns:1fr 1fr!important}.toolbar-modern #routeSelect{grid-column:1 / -1!important}.toolbar-modern .btn-kpi{grid-column:1 / 2!important}.toolbar-modern .nav-btn{grid-column:2 / 3!important;width:100%!important}.kpi-grid-modal{row-gap:10px}.kpi-grid-modal .col-6{width:50%;}.kpi-grid-modal .col-12{width:100%;}}
@media (max-width:420px){.kpi-grid-modal .col-6{width:100%;}}

.reload-help{background:#eff6ff;border:1px solid #bfdbfe;color:#0f3b63;border-radius:16px;padding:12px 14px;margin-bottom:12px;font-weight:750;line-height:1.35}.reload-card-row{background:linear-gradient(180deg,#ffffff,#f8fbff)}.calc-preview.ok{border-color:#bbf7d0;background:#f0fdf4;color:#065f46}
</style>
</head>
<body>
<div class="screen-actions"><button onclick="window.print()">Imprimir ticket</button><button onclick="window.close()">Cerrar</button></div>
<div class="ticket">
  <div class="center">
    <div class="title">Nota de entrega</div>
    <div class="sub"><?=h_ticket_reparto($scopeCompanyName)?> · <?=h_ticket_reparto($scopeLocationName)?></div>
    <div class="sub bold"><?=h_ticket_reparto((string)($out['reference'] ?? ''))?></div>
  </div>
  <div class="line"></div>
  <div class="meta">
    <div class="bold">Fecha:</div><div><?=h_ticket_reparto($dt->format('d/m/Y'))?></div>
    <div class="bold">Hora:</div><div><?=h_ticket_reparto($dt->format('H:i:s'))?></div>
    <div class="bold">Cliente:</div><div><?=h_ticket_reparto((string)($out['customer_name'] ?? ''))?></div>
    <div class="bold">Repartidor:</div><div><?=h_ticket_reparto($seller)?></div>
    <div class="bold">Ruta:</div><div><?=h_ticket_reparto($route)?></div>
    <div class="bold">Unidad:</div><div><?=h_ticket_reparto((string)($out['vehicle_unit'] ?? ''))?></div>
  </div>
  <div class="line"></div>
  <table class="items">
    <thead><tr><th style="width:58%">Artículo</th><th class="right">Pz</th><th class="right">Paq</th><th class="right">Tar</th></tr></thead>
    <tbody>
    <?php foreach ($items as $it):
        $qty = (float)$it['qty'];
        $ppp = (int)($it['pieces_per_package'] ?? 0);
        $ppal = (int)($it['packages_per_pallet'] ?? 0);
        $packages = $ppp > 0 ? ($qty / $ppp) : 0;
        $pallets = ($ppp > 0 && $ppal > 0) ? ($qty / ($ppp * $ppal)) : 0;
    ?>
      <tr>
        <td><div class="sku"><?=h_ticket_reparto((string)$it['sku'])?></div><div class="name"><?=h_ticket_reparto((string)$it['name'])?></div></td>
        <td class="right bold"><?=fmt_ticket_qty_reparto($qty, 0)?></td>
        <td class="right"><?=fmt_ticket_qty_reparto($packages, 2)?></td>
        <td class="right"><?=fmt_ticket_qty_reparto($pallets, 2)?></td>
      </tr>
    <?php endforeach; ?>
    </tbody>
  </table>
  <table class="totals">
    <tr><td class="bold">Total piezas</td><td class="right bold"><?=fmt_ticket_qty_reparto($totalPieces, 0)?></td></tr>
    <tr><td>Total paquetes</td><td class="right"><?=fmt_ticket_qty_reparto($totalPackages, 2)?></td></tr>
    <tr><td>Total tarimas</td><td class="right"><?=fmt_ticket_qty_reparto($totalPallets, 2)?></td></tr>
  </table>
  <?php if (trim((string)($out['notes'] ?? '')) !== ''): ?>
    <div class="line no-print"></div>
    <div class="bold no-print">Notas:</div>
    <div class="muted no-print"><?=h_ticket_reparto((string)$out['notes'])?></div>
  <?php endif; ?>
  <div class="signature"></div>
  <div class="center sub">Firma / recibido</div>
  <div class="footer">BReport · Reparto Botella</div>
</div>
<script>setTimeout(()=>{try{window.print();}catch(e){}},350);</script>
</body>
</html>
    <?php
    exit;
}

function breport_operational_start_hour_reparto(): int { return 7; }

function breport_operational_today_reparto(): string {
    $now = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE));
    if ((int)$now->format('H') < breport_operational_start_hour_reparto()) $now = $now->modify('-1 day');
    return $now->format('Y-m-d');
}

function breport_resolve_work_date_reparto(string $submittedDate): string {
    $submittedDate = trim($submittedDate);
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $submittedDate)) return breport_operational_today_reparto();
    $now = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TIMEZONE));
    if ((int)$now->format('H') < breport_operational_start_hour_reparto() && $submittedDate === $now->format('Y-m-d')) {
        return breport_operational_today_reparto();
    }
    return $submittedDate;
}

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

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

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

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

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

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

function ensure_repartidor_botella_schema(PDO $pdo): void {
    if (!table_exists_reparto($pdo, 'botella_route_user_assignments')) {
        $pdo->exec("CREATE TABLE botella_route_user_assignments (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            location_id BIGINT UNSIGNED NOT NULL,
            user_id BIGINT UNSIGNED NOT NULL,
            route_no INT NOT NULL,
            warehouse_id BIGINT UNSIGNED DEFAULT NULL,
            active TINYINT(1) NOT NULL DEFAULT 1,
            assigned_by BIGINT UNSIGNED DEFAULT NULL,
            assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            notes VARCHAR(255) DEFAULT NULL,
            PRIMARY KEY(id),
            KEY idx_bru_active_user (company_id, location_id, user_id, active),
            KEY idx_bru_route (company_id, location_id, route_no),
            KEY idx_bru_user (user_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }

    if (!table_exists_reparto($pdo, 'botella_route_reloads')) {
        $pdo->exec("CREATE TABLE botella_route_reloads (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            location_id BIGINT UNSIGNED NOT NULL,
            route_no INT NOT NULL,
            route_warehouse_id BIGINT UNSIGNED NOT NULL,
            work_date DATE NOT NULL,
            status VARCHAR(30) NOT NULL DEFAULT 'PENDIENTE',
            requested_by BIGINT UNSIGNED NOT NULL,
            approved_by BIGINT UNSIGNED DEFAULT NULL,
            approved_at DATETIME DEFAULT NULL,
            shipped_by BIGINT UNSIGNED DEFAULT NULL,
            shipped_at DATETIME DEFAULT NULL,
            notes VARCHAR(255) DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT NULL,
            PRIMARY KEY(id),
            KEY idx_brr_scope_status (company_id, location_id, status),
            KEY idx_brr_route_date (company_id, location_id, route_no, work_date)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }

    if (!table_exists_reparto($pdo, 'botella_route_reload_items')) {
        $pdo->exec("CREATE TABLE botella_route_reload_items (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            reload_id BIGINT UNSIGNED NOT NULL,
            product_id BIGINT UNSIGNED NOT NULL,
            packaging_option_id BIGINT UNSIGNED DEFAULT NULL,
            requested_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            requested_packages INT UNSIGNED DEFAULT NULL,
            requested_pallets INT UNSIGNED DEFAULT NULL,
            shipped_qty DECIMAL(14,3) DEFAULT NULL,
            source_warehouse_id BIGINT UNSIGNED DEFAULT NULL,
            inventory_move_out_id BIGINT UNSIGNED DEFAULT NULL,
            inventory_move_in_id BIGINT UNSIGNED DEFAULT NULL,
            notes VARCHAR(255) DEFAULT NULL,
            PRIMARY KEY(id),
            KEY idx_brri_reload (reload_id),
            KEY idx_brri_product (product_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }

    if (!table_exists_reparto($pdo, 'botella_route_day_closures')) {
        $pdo->exec("CREATE TABLE botella_route_day_closures (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            location_id BIGINT UNSIGNED NOT NULL,
            route_no INT NOT NULL,
            route_warehouse_id BIGINT UNSIGNED NOT NULL,
            work_date DATE NOT NULL,
            closed_by BIGINT UNSIGNED NOT NULL,
            closed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            notes VARCHAR(255) DEFAULT NULL,
            PRIMARY KEY(id),
            UNIQUE KEY uk_brdc_route_day (company_id, location_id, route_no, work_date),
            KEY idx_brdc_user (closed_by)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }
}

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

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

function botella_route_row_reparto(PDO $pdo, int $companyId, int $locationId, int $routeNo): array {
    $st = $pdo->prepare('SELECT id, route_no, route_code, route_name, driver_name, vehicle_unit FROM route_access_codes WHERE company_id=? AND location_id=? AND route_no=? AND active=1 LIMIT 1');
    $st->execute([$companyId, $locationId, $routeNo]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) throw new RuntimeException('La ruta #' . $routeNo . ' no está activa para esta planta.');
    return $row;
}

function botella_ensure_route_warehouse_reparto(PDO $pdo, int $companyId, int $locationId, int $routeNo): int {
    if ($routeNo <= 0) throw new RuntimeException('Ruta inválida.');
    $code = botella_route_warehouse_code_reparto($routeNo);
    $st = $pdo->prepare('SELECT id FROM warehouses WHERE company_id=? AND location_id=? AND UPPER(code)=UPPER(?) LIMIT 1');
    $st->execute([$companyId, $locationId, $code]);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id > 0) return $id;

    $route = botella_route_row_reparto($pdo, $companyId, $locationId, $routeNo);
    $name = 'Ruta ' . $routeNo . ' · ' . trim((string)($route['route_code'] ?? '') . ' ' . (string)($route['route_name'] ?? ''));
    $st = $pdo->prepare('INSERT INTO warehouses(company_id, location_id, code, name, active, created_at) VALUES(?, ?, ?, ?, 1, NOW())');
    $st->execute([$companyId, $locationId, $code, $name]);
    return (int)$pdo->lastInsertId();
}

function botella_alm01_warehouse_id_reparto(PDO $pdo, int $companyId, int $locationId): int {
    $st = $pdo->prepare("SELECT id FROM warehouses WHERE company_id=? AND location_id=? AND active=1 AND UPPER(code)='ALM01' LIMIT 1");
    $st->execute([$companyId, $locationId]);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id <= 0) throw new RuntimeException('No existe ALM01 activo para recibir devoluciones. Configura el almacén físico principal.');
    return $id;
}


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

function botella_assert_route_warehouse_reparto(PDO $pdo, int $companyId, int $locationId, array &$ctx): void {
    /*
     * Candado crítico Reparto Botella:
     * Una entrega de repartidor NUNCA debe tomar ALM01 ni almacenes físicos.
     * La salida del cliente siempre debe salir del mini-almacén móvil RUTA-xx.
     */
    $routeNo = (int)($ctx['route_no'] ?? 0);
    if ($routeNo <= 0) {
        throw new RuntimeException('No se pudo resolver la ruta del repartidor.');
    }

    $routeWarehouseId = botella_ensure_route_warehouse_reparto($pdo, $companyId, $locationId, $routeNo);
    $routeWarehouseCode = botella_warehouse_code_by_id_reparto($pdo, $companyId, $locationId, $routeWarehouseId);
    $expectedCode = botella_route_warehouse_code_reparto($routeNo);

    if ($routeWarehouseId <= 0 || $routeWarehouseCode === '') {
        throw new RuntimeException('No se pudo resolver el almacén móvil de la ruta ' . $expectedCode . '.');
    }

    if (!botella_is_route_warehouse_code_reparto($routeWarehouseCode) || $routeWarehouseCode === 'ALM01') {
        throw new RuntimeException('Bloqueo de seguridad: Reparto intentó usar almacén no móvil (' . $routeWarehouseCode . '). La salida debe salir de ' . $expectedCode . ', no de ALM01.');
    }

    if (strtoupper($expectedCode) !== $routeWarehouseCode) {
        throw new RuntimeException('La ruta seleccionada no coincide con el almacén móvil. Esperado: ' . $expectedCode . ', resuelto: ' . $routeWarehouseCode . '.');
    }

    $ctx['warehouse_id'] = $routeWarehouseId;
    $ctx['warehouse_code'] = $routeWarehouseCode;
}



function botella_reparto_channel_id(PDO $pdo, int $companyId): int {
    /*
     * outbounds.channel_id tiene llave foránea contra outbound_channels.id.
     * No se debe insertar 0. Se reutiliza o se crea un canal propio para entregas de reparto Botella.
     */
    $channelName = 'REPARTO BOTELLA';
    $st = $pdo->prepare("SELECT id FROM outbound_channels WHERE company_id=? AND UPPER(name)=UPPER(?) LIMIT 1");
    $st->execute([$companyId, $channelName]);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id > 0) return $id;

    $st = $pdo->prepare("INSERT INTO outbound_channels(company_id, name, channel_type, active, sort_order, conversion_enabled) VALUES(?, ?, 'SALE', 1, 90, 0)");
    $st->execute([$companyId, $channelName]);
    return (int)$pdo->lastInsertId();
}

function current_route_assignment_reparto(PDO $pdo, int $companyId, int $locationId, int $userId): ?array {
    $st = $pdo->prepare("SELECT a.*, w.code AS warehouse_code, w.name AS warehouse_name, r.route_code, r.route_name, r.driver_name, r.vehicle_unit
        FROM botella_route_user_assignments a
        LEFT JOIN warehouses w ON w.id=a.warehouse_id
        LEFT JOIN route_access_codes r ON r.company_id=a.company_id AND r.location_id=a.location_id AND r.route_no=a.route_no AND r.active=1
        WHERE a.company_id=? AND a.location_id=? AND a.user_id=? AND a.active=1
        ORDER BY a.id DESC LIMIT 1");
    $st->execute([$companyId, $locationId, $userId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
}

function resolve_route_context_reparto(PDO $pdo, int $companyId, int $locationId, array $user): array {
    $routeNo = (int)($_GET['route_no'] ?? 0);
    $userId = (int)$user['id'];
    $isAdmin = has_role('admin') || has_role('warehouse') || has_role('almacen');

    if ($routeNo <= 0) {
        $assignment = current_route_assignment_reparto($pdo, $companyId, $locationId, $userId);
        if ($assignment) $routeNo = (int)$assignment['route_no'];
    }

    if ($routeNo <= 0 && $isAdmin) {
        $st = $pdo->prepare("SELECT route_no FROM route_access_codes WHERE company_id=? AND location_id=? AND active=1 AND NOT (route_no = 1 OR UPPER(route_code) LIKE '%VENTA%' OR UPPER(route_name) LIKE '%VENTA%') ORDER BY sort_order, route_no LIMIT 1");
        $st->execute([$companyId, $locationId]);
        $routeNo = (int)($st->fetchColumn() ?: 0);
    }

    if ($routeNo <= 0) {
        throw new RuntimeException('Tu usuario no tiene ruta asignada. Pide a Admin/Almacén asignarte una ruta en botella_route_user_assignments.');
    }

    if (!$isAdmin) {
        $assignment = current_route_assignment_reparto($pdo, $companyId, $locationId, $userId);
        if (!$assignment || (int)$assignment['route_no'] !== $routeNo) {
            throw new RuntimeException('No tienes permiso para operar esta ruta.');
        }
    }

    $route = botella_route_row_reparto($pdo, $companyId, $locationId, $routeNo);
    $warehouseId = botella_ensure_route_warehouse_reparto($pdo, $companyId, $locationId, $routeNo);
    $code = botella_route_warehouse_code_reparto($routeNo);
    return ['route_no'=>$routeNo, 'warehouse_id'=>$warehouseId, 'warehouse_code'=>$code, 'route'=>$route];
}

function route_stock_reparto(PDO $pdo, int $companyId, int $locationId, int $routeWarehouseId): array {
    $st = $pdo->prepare("SELECT p.id AS product_id, p.sku, p.name, p.uom,
            COALESCE(ppo.id,0) AS packaging_option_id,
            COALESCE(ppo.packaging_name, CONCAT(COALESCE(p.packages_per_pallet,0),' paquetes por tarima')) AS packaging_name,
            COALESCE(ppo.pieces_per_package, p.pieces_per_package, 0) AS pieces_per_package,
            COALESCE(ppo.packages_per_pallet, p.packages_per_pallet, 0) AS packages_per_pallet,
            COALESCE(SUM(CASE WHEN im.move_type IN ('IN','ADJ') THEN im.qty WHEN im.move_type='OUT' THEN -im.qty ELSE 0 END),0) AS qty
        FROM products p
        LEFT JOIN product_packaging_options ppo ON ppo.company_id=p.company_id AND ppo.product_id=p.id AND ppo.active=1 AND ppo.is_default=1
        LEFT JOIN inventory_moves im ON im.company_id=p.company_id AND im.location_id=? AND im.product_id=p.id AND im.warehouse_id=?
        WHERE p.company_id=? AND p.active=1
        GROUP BY p.id, p.sku, p.name, p.uom, ppo.id, ppo.packaging_name, ppo.pieces_per_package, ppo.packages_per_pallet, p.pieces_per_package, p.packages_per_pallet
        HAVING qty <> 0
        ORDER BY p.sku, p.name");
    $st->execute([$locationId, $routeWarehouseId, $companyId]);
    $rows = [];
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
        $qty = (float)$r['qty'];
        $piecesPerPackage = (int)($r['pieces_per_package'] ?? 0);
        $packagesPerPallet = (int)($r['packages_per_pallet'] ?? 0);
        $packages = $piecesPerPackage > 0 ? floor($qty / $piecesPerPackage) : 0;
        $pallets = ($piecesPerPackage > 0 && $packagesPerPallet > 0) ? floor($qty / ($piecesPerPackage * $packagesPerPallet)) : 0;
        $rows[] = [
            'product_id'=>(int)$r['product_id'], 'sku'=>(string)$r['sku'], 'name'=>(string)$r['name'], 'uom'=>(string)$r['uom'],
            'qty'=>round($qty,3), 'packages'=>(int)$packages, 'pallets'=>(int)$pallets,
            'packaging_option_id'=>(int)$r['packaging_option_id'], 'packaging_name'=>(string)$r['packaging_name'],
            'pieces_per_package'=>$piecesPerPackage, 'packages_per_pallet'=>$packagesPerPallet
        ];
    }
    return $rows;
}

function route_product_available_reparto(PDO $pdo, int $companyId, int $locationId, int $routeWarehouseId, int $productId): float {
    $st = $pdo->prepare("SELECT COALESCE(SUM(CASE WHEN move_type IN ('IN','ADJ') THEN qty WHEN move_type='OUT' THEN -qty ELSE 0 END),0)
        FROM inventory_moves WHERE company_id=? AND location_id=? AND warehouse_id=? AND product_id=?");
    $st->execute([$companyId, $locationId, $routeWarehouseId, $productId]);
    return (float)$st->fetchColumn();
}

function route_day_summary_reparto(PDO $pdo, int $companyId, int $locationId, int $routeWarehouseId, string $workDate): array {
    $win = breport_operational_window_reparto($workDate);
    $st = $pdo->prepare("SELECT p.id AS product_id, p.sku, p.name,
            COALESCE(ppo.pieces_per_package, p.pieces_per_package, 0) AS pieces_per_package,
            COALESCE(ppo.packages_per_pallet, p.packages_per_pallet, 0) AS packages_per_pallet,
            SUM(CASE WHEN im.move_type IN ('IN','ADJ') AND im.created_at < ? THEN im.qty WHEN im.move_type='OUT' AND im.created_at < ? THEN -im.qty ELSE 0 END) AS initial_qty,
            SUM(CASE WHEN im.move_type IN ('IN','ADJ') AND im.created_at >= ? AND im.created_at < ? THEN im.qty ELSE 0 END) AS in_qty,
            SUM(CASE WHEN im.move_type='OUT' AND im.created_at >= ? AND im.created_at < ? THEN im.qty ELSE 0 END) AS out_qty,
            SUM(CASE WHEN im.move_type IN ('IN','ADJ') THEN im.qty WHEN im.move_type='OUT' THEN -im.qty ELSE 0 END) AS current_qty
        FROM inventory_moves im
        JOIN products p ON p.id=im.product_id AND p.company_id=im.company_id
        LEFT JOIN product_packaging_options ppo ON ppo.company_id=p.company_id AND ppo.product_id=p.id AND ppo.active=1 AND ppo.is_default=1
        WHERE im.company_id=? AND im.location_id=? AND im.warehouse_id=?
        GROUP BY p.id, p.sku, p.name, ppo.pieces_per_package, ppo.packages_per_pallet, p.pieces_per_package, p.packages_per_pallet
        HAVING initial_qty <> 0 OR in_qty <> 0 OR out_qty <> 0 OR current_qty <> 0
        ORDER BY p.sku, p.name");
    $st->execute([$win['start'],$win['start'],$win['start'],$win['end'],$win['start'],$win['end'],$companyId,$locationId,$routeWarehouseId]);
    return array_map(static function(array $r): array {
        $piecesPerPackage = (int)($r['pieces_per_package'] ?? 0);
        $packagesPerPallet = (int)($r['packages_per_pallet'] ?? 0);
        $currentQty = (float)$r['current_qty'];
        $currentPackages = $piecesPerPackage > 0 ? floor($currentQty / $piecesPerPackage) : 0;
        $currentPallets = ($piecesPerPackage > 0 && $packagesPerPallet > 0) ? floor($currentQty / ($piecesPerPackage * $packagesPerPallet)) : 0;
        return [
            'product_id'=>(int)$r['product_id'],
            'sku'=>(string)$r['sku'],
            'name'=>(string)$r['name'],
            'pieces_per_package'=>$piecesPerPackage,
            'packages_per_pallet'=>$packagesPerPallet,
            'initial_qty'=>(float)$r['initial_qty'],
            'in_qty'=>(float)$r['in_qty'],
            'out_qty'=>(float)$r['out_qty'],
            'current_qty'=>$currentQty,
            'current_packages'=>(int)$currentPackages,
            'current_pallets'=>(int)$currentPallets,
        ];
    }, $st->fetchAll(PDO::FETCH_ASSOC));
}


function product_packaging_factor_reparto(PDO $pdo, int $companyId, int $productId): array {
    $st = $pdo->prepare("SELECT
            COALESCE(ppo.pieces_per_package, p.pieces_per_package, 0) AS pieces_per_package,
            COALESCE(ppo.packages_per_pallet, p.packages_per_pallet, 0) AS packages_per_pallet
        FROM products p
        LEFT JOIN product_packaging_options ppo ON ppo.company_id=p.company_id AND ppo.product_id=p.id AND ppo.active=1 AND ppo.is_default=1
        WHERE p.company_id=? AND p.id=? AND p.active=1
        LIMIT 1");
    $st->execute([$companyId, $productId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) throw new RuntimeException('Producto inválido para la salida.');
    return [
        'pieces_per_package' => (int)($row['pieces_per_package'] ?? 0),
        'packages_per_pallet' => (int)($row['packages_per_pallet'] ?? 0),
    ];
}

function route_unit_to_pieces_reparto(PDO $pdo, int $companyId, int $productId, string $unitType, float $amount): float {
    $unitType = strtolower(trim($unitType));
    if ($amount <= 0) throw new RuntimeException('La cantidad debe ser mayor a cero.');
    if ($unitType === '') $unitType = 'pieza';
    $pkg = product_packaging_factor_reparto($pdo, $companyId, $productId);
    $piecesPerPackage = (int)$pkg['pieces_per_package'];
    $packagesPerPallet = (int)$pkg['packages_per_pallet'];

    if (in_array($unitType, ['pieza','piezas','piece','pieces'], true)) {
        $pieces = $amount;
    } elseif (in_array($unitType, ['paquete','paquetes','package','packages'], true)) {
        if ($piecesPerPackage <= 0) throw new RuntimeException('El producto no tiene piezas por paquete configuradas.');
        $pieces = $amount * $piecesPerPackage;
    } elseif (in_array($unitType, ['tarima','tarimas','pallet','pallets'], true)) {
        if ($piecesPerPackage <= 0 || $packagesPerPallet <= 0) throw new RuntimeException('El producto no tiene embalaje por tarima configurado.');
        $pieces = $amount * $packagesPerPallet * $piecesPerPackage;
    } else {
        throw new RuntimeException('Unidad de entrega no válida. Usa tarima, paquete o piezas.');
    }

    $rounded = round($pieces);
    if ($rounded <= 0) throw new RuntimeException('La cantidad calculada debe ser mayor a cero piezas.');
    if (abs($pieces - $rounded) > 0.000001) {
        // En Botella el movimiento oficial se controla en piezas completas.
        // Para fracciones de tarima como 0.2, se redondea al entero más cercano para poder registrar la salida real.
        $pieces = $rounded;
    }
    validate_qty_positive_reparto((float)$pieces);
    return (float)$pieces;
}

function validate_qty_positive_reparto(float $qty): void {
    if ($qty <= 0) throw new RuntimeException('La cantidad debe ser mayor a cero.');
    if (abs($qty - round($qty)) > 0.000001) throw new RuntimeException('Botella trabaja piezas completas. No se permiten decimales.');
}


function reparto_all_active_products(PDO $pdo, int $companyId): array {
    /*
     * Para solicitudes de recarga, el repartidor puede pedir cualquier artículo
     * activo de su planta Botella, aunque actualmente no lo tenga cargado en ruta.
     */
    $join = '';
    $selectPpo = "0 AS packaging_option_id, NULL AS packaging_name, p.pieces_per_package, p.packages_per_pallet";
    if (table_exists_reparto($pdo, 'product_packaging_options')) {
        $join = "LEFT JOIN product_packaging_options ppo ON ppo.company_id=p.company_id AND ppo.product_id=p.id AND ppo.active=1 AND ppo.is_default=1";
        $selectPpo = "COALESCE(ppo.id,0) AS packaging_option_id,
            COALESCE(ppo.packaging_name, CONCAT(COALESCE(p.packages_per_pallet,0),' paquetes por tarima')) AS packaging_name,
            COALESCE(ppo.pieces_per_package, p.pieces_per_package, 0) AS pieces_per_package,
            COALESCE(ppo.packages_per_pallet, p.packages_per_pallet, 0) AS packages_per_pallet";
    }
    $st = $pdo->prepare("SELECT p.id AS product_id, p.sku, p.name, p.uom, {$selectPpo}
        FROM products p
        {$join}
        WHERE p.company_id=? AND p.active=1
        ORDER BY p.sku, p.name");
    $st->execute([$companyId]);
    return array_map(static function(array $r): array {
        return [
            'product_id'=>(int)$r['product_id'],
            'sku'=>(string)$r['sku'],
            'name'=>(string)$r['name'],
            'uom'=>(string)($r['uom'] ?? 'pz'),
            'qty'=>0,
            'packages'=>0,
            'pallets'=>0,
            'packaging_option_id'=>(int)($r['packaging_option_id'] ?? 0),
            'packaging_name'=>(string)($r['packaging_name'] ?? ''),
            'pieces_per_package'=>(int)($r['pieces_per_package'] ?? 0),
            'packages_per_pallet'=>(int)($r['packages_per_pallet'] ?? 0),
        ];
    }, $st->fetchAll(PDO::FETCH_ASSOC));
}

/* ============================================================
   API
============================================================ */
if ($action !== '') {
    try {
        need_csrf_reparto();
        $ctx = resolve_route_context_reparto($pdo, $companyId, $locationId, $u);
        botella_assert_route_warehouse_reparto($pdo, $companyId, $locationId, $ctx);
        $workDate = breport_resolve_work_date_reparto((string)($_GET['work_date'] ?? breport_operational_today_reparto()));

        if ($action === 'bootstrap') {
            $routes = [];
            if (has_role('admin') || has_role('warehouse') || has_role('almacen')) {
                $st = $pdo->prepare("SELECT route_no, route_code, route_name, driver_name, vehicle_unit FROM route_access_codes WHERE company_id=? AND location_id=? AND active=1 AND NOT (route_no = 1 OR UPPER(route_code) LIKE '%VENTA%' OR UPPER(route_name) LIKE '%VENTA%') ORDER BY sort_order, route_no");
                $st->execute([$companyId, $locationId]);
                $routes = $st->fetchAll(PDO::FETCH_ASSOC);
            }
            jexit_reparto(['ok'=>true, 'user'=>['id'=>(int)$u['id'], 'name'=>(string)($u['full_name'] ?? $u['username'] ?? '')], 'scope'=>['company'=>$GLOBALS['scopeCompanyName'], 'location'=>$GLOBALS['scopeLocationName']], 'work_date'=>$workDate, 'route'=>$ctx, 'routes'=>$routes, 'reload_products'=>reparto_all_active_products($pdo, $companyId), 'csrf'=>csrf_token()]);
        }

        if ($action === 'inventory') {
            jexit_reparto(['ok'=>true, 'route'=>$ctx, 'work_date'=>$workDate, 'summary'=>route_day_summary_reparto($pdo, $companyId, $locationId, (int)$ctx['warehouse_id'], $workDate), 'stock'=>route_stock_reparto($pdo, $companyId, $locationId, (int)$ctx['warehouse_id'])]);
        }

        if ($action === 'customers') {
            $q = trim((string)($_GET['q'] ?? ''));
            $sql = "SELECT id, customer_code, business_name, contact_name, phone, address FROM sales_customers WHERE company_id=? AND active=1";
            $params = [$companyId];
            if (column_exists_reparto($pdo, 'sales_customers', 'location_id')) { $sql .= " AND (location_id IS NULL OR location_id=?)"; $params[] = $locationId; }
            if ($q !== '') { $sql .= " AND (business_name LIKE ? OR customer_code LIKE ? OR contact_name LIKE ?)"; $like = '%' . $q . '%'; array_push($params, $like, $like, $like); }
            $sql .= " ORDER BY business_name LIMIT 50";
            $st = $pdo->prepare($sql); $st->execute($params);
            jexit_reparto(['ok'=>true, 'rows'=>$st->fetchAll(PDO::FETCH_ASSOC)]);
        }

        if ($action === 'delivery_ticket') {
            $outboundId = (int)($_GET['outbound_id'] ?? 0);
            print_delivery_ticket_reparto($pdo, $companyId, $locationId, $outboundId, $GLOBALS['scopeCompanyName'], $GLOBALS['scopeLocationName']);
        }

        if ($action === 'route_out_save') {
            $d = read_json_reparto();
            $customerName = trim((string)($d['customer_name'] ?? ''));
            // La referencia/ticket de reparto se genera siempre por sistema.
            // No se recibe desde pantalla para evitar capturas manuales duplicadas o inconsistentes.
            $reference = botella_delivery_reference_reparto((int)$ctx['route_no'], (int)$u['id']);
            $notes = trim((string)($d['notes'] ?? ''));

            $items = $d['items'] ?? null;
            if (!is_array($items)) {
                $productId = (int)($d['product_id'] ?? 0);
                $qty = (float)($d['qty'] ?? 0);
                $items = [['product_id' => $productId, 'qty' => $qty]];
            }

            if ($customerName === '') throw new RuntimeException('Cliente/destino requerido.');
            if (!$items) throw new RuntimeException('Agrega al menos un artículo a entregar.');

            $agg = [];
            foreach ($items as $it) {
                $productId = (int)($it['product_id'] ?? 0);
                if ($productId <= 0) throw new RuntimeException('Producto requerido en la salida.');

                $unitType = strtolower(trim((string)($it['unit_type'] ?? 'pieza')));
                $amount = (float)($it['amount'] ?? $it['qty'] ?? 0);
                $qty = route_unit_to_pieces_reparto($pdo, $companyId, $productId, $unitType, $amount);

                if (!isset($agg[$productId])) $agg[$productId] = 0.0;
                $agg[$productId] = round($agg[$productId] + $qty, 3);
            }

            $pdo->beginTransaction();
            foreach ($agg as $productId => $qty) {
                $available = route_product_available_reparto($pdo, $companyId, $locationId, (int)$ctx['warehouse_id'], (int)$productId);
                if ($qty > $available + 0.000001) {
                    throw new RuntimeException('Inventario insuficiente en ' . $ctx['warehouse_code'] . ' para producto #' . $productId . '. Disponible: ' . number_format($available, 0) . ', solicitado: ' . number_format($qty, 0));
                }
            }

            $route = $ctx['route'];
            $channelId = botella_reparto_channel_id($pdo, $companyId);
            $fullNotes = 'Entrega en ruta ' . $ctx['warehouse_code'] . ' | Almacén móvil ID: ' . (int)$ctx['warehouse_id'] . ' | Cliente/Destino: ' . $customerName;
            $fullNotes .= ' | Ref: ' . $reference;
            if ($notes !== '') $fullNotes .= ' | Notas: ' . $notes;

            $st = $pdo->prepare("INSERT INTO outbounds(company_id, location_id, outbound_date, channel_id, warehouse_id, customer_name, driver_name, vehicle_unit, depart_at, status, created_by, reference, notes, created_at)
                VALUES(?, ?, ?, ?, ?, ?, ?, ?, NOW(), 'POSTED', ?, ?, ?, NOW())");
            $st->execute([$companyId, $locationId, $workDate, $channelId, (int)$ctx['warehouse_id'], $customerName, (string)($route['driver_name'] ?? ''), (string)($route['vehicle_unit'] ?? ''), (int)$u['id'], $reference, $fullNotes]);
            $outboundId = (int)$pdo->lastInsertId();

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

            foreach ($agg as $productId => $qty) {
                $stItem->execute([$outboundId, (int)$productId, $qty]);
                $stMove->execute([$companyId, $locationId, (int)$ctx['warehouse_id'], $workDate, (int)$productId, $qty, $channelId, $outboundId, $fullNotes, (int)$u['id']]);
            }

            $pdo->commit();
            $ticketUrl = '?action=delivery_ticket&outbound_id=' . $outboundId . '&route_no=' . (int)$ctx['route_no'] . '&work_date=' . rawurlencode($workDate);
            jexit_reparto(['ok'=>true, 'outbound_id'=>$outboundId, 'reference'=>$reference, 'ticket_url'=>$ticketUrl, 'message'=>'Salida registrada con ' . count($agg) . ' artículo(s). Referencia: ' . $reference]);
        }

        if ($action === 'reload_request_save') {
            $d = read_json_reparto();
            $items = $d['items'] ?? [];
            $notes = trim((string)($d['notes'] ?? ''));
            if (!is_array($items) || !$items) throw new RuntimeException('Agrega al menos un artículo para solicitar recarga.');

            $pdo->beginTransaction();
            $st = $pdo->prepare("INSERT INTO botella_route_reloads(company_id, location_id, route_no, route_warehouse_id, work_date, status, requested_by, notes, created_at)
                VALUES(?, ?, ?, ?, ?, 'PENDIENTE', ?, ?, NOW())");
            $st->execute([$companyId, $locationId, (int)$ctx['route_no'], (int)$ctx['warehouse_id'], $workDate, (int)$u['id'], $notes]);
            $reloadId = (int)$pdo->lastInsertId();

            $stItem = $pdo->prepare("INSERT INTO botella_route_reload_items(reload_id, product_id, packaging_option_id, requested_qty, requested_packages, requested_pallets, notes) VALUES(?, ?, ?, ?, ?, ?, ?)");
            foreach ($items as $it) {
                $productId = (int)($it['product_id'] ?? 0);
                $qty = (float)($it['qty'] ?? 0);
                validate_qty_positive_reparto($qty);
                if ($productId <= 0) throw new RuntimeException('Artículo inválido en solicitud.');
                $stItem->execute([$reloadId, $productId, ((int)($it['packaging_option_id'] ?? 0) ?: null), $qty, ((int)($it['packages_qty'] ?? 0) ?: null), ((int)($it['pallets_qty'] ?? 0) ?: null), trim((string)($it['notes'] ?? '')) ?: null]);
            }
            $pdo->commit();
            jexit_reparto(['ok'=>true, 'reload_id'=>$reloadId, 'message'=>'Solicitud de recarga enviada a Almacén.']);
        }

        if ($action === 'return_to_warehouse_save') {
            $d = read_json_reparto();
            $items = $d['items'] ?? [];
            $notes = trim((string)($d['notes'] ?? ''));
            if (!is_array($items) || !$items) throw new RuntimeException('Agrega al menos un artículo a devolver.');
            $destWarehouseId = botella_alm01_warehouse_id_reparto($pdo, $companyId, $locationId);

            $pdo->beginTransaction();
            $refId = 0;
            $fullNotesBase = 'Devolución de ruta ' . $ctx['warehouse_code'] . ' a ALM01';
            if ($notes !== '') $fullNotesBase .= ' | Notas: ' . $notes;
            foreach ($items as $it) {
                $productId = (int)($it['product_id'] ?? 0);
                $qty = (float)($it['qty'] ?? 0);
                validate_qty_positive_reparto($qty);
                if ($productId <= 0) throw new RuntimeException('Artículo inválido en devolución.');
                $available = route_product_available_reparto($pdo, $companyId, $locationId, (int)$ctx['warehouse_id'], $productId);
                if ($qty > $available + 0.000001) throw new RuntimeException('No puedes devolver más de lo disponible en ruta. Producto #' . $productId . ' disponible: ' . number_format($available, 0));

                $st = $pdo->prepare("INSERT INTO inventory_moves(company_id, location_id, warehouse_id, work_date, product_id, qty, move_type, channel_id, ref_type, ref_id, notes, created_by, created_at)
                    VALUES(?, ?, ?, ?, ?, ?, 'OUT', NULL, 'ROUTE_RETURN_OUT', ?, ?, ?, NOW())");
                $st->execute([$companyId, $locationId, (int)$ctx['warehouse_id'], $workDate, $productId, $qty, $refId, $fullNotesBase, (int)$u['id']]);
                $outMoveId = (int)$pdo->lastInsertId();
                if ($refId === 0) $refId = $outMoveId;

                $st = $pdo->prepare("INSERT INTO inventory_moves(company_id, location_id, warehouse_id, work_date, product_id, qty, move_type, channel_id, ref_type, ref_id, notes, created_by, created_at)
                    VALUES(?, ?, ?, ?, ?, ?, 'IN', NULL, 'ROUTE_RETURN_IN', ?, ?, ?, NOW())");
                $st->execute([$companyId, $locationId, $destWarehouseId, $workDate, $productId, $qty, $refId, $fullNotesBase, (int)$u['id']]);
            }
            $pdo->commit();
            jexit_reparto(['ok'=>true, 'message'=>'Devolución registrada. El producto volvió a ALM01 y ya puede considerarse disponible general.']);
        }

        if ($action === 'close_day') {
            $d = read_json_reparto();
            $notes = trim((string)($d['notes'] ?? ''));
            $st = $pdo->prepare("INSERT INTO botella_route_day_closures(company_id, location_id, route_no, route_warehouse_id, work_date, closed_by, notes, closed_at)
                VALUES(?, ?, ?, ?, ?, ?, ?, NOW())
                ON DUPLICATE KEY UPDATE closed_by=VALUES(closed_by), notes=VALUES(notes), closed_at=NOW()");
            $st->execute([$companyId, $locationId, (int)$ctx['route_no'], (int)$ctx['warehouse_id'], $workDate, (int)$u['id'], $notes]);
            jexit_reparto(['ok'=>true, 'message'=>'Ruta cerrada. El saldo que quedó en ruta permanece como inventario inicial del siguiente día operativo.']);
        }

        jexit_reparto(['ok'=>false, 'error'=>'Acción no reconocida.'], 404);
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        jexit_reparto(['ok'=>false, 'error'=>$e->getMessage()], 400);
    }
}

$csrf = csrf_token();
$defaultWorkDate = breport_operational_today_reparto();
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BReport · Reparto Botella</title>
<style>
:root{
  --brand:#0f3b63;--brand2:#0b2545;--accent:#1d75bd;--ok:#12a150;--warn:#f59e0b;--danger:#dc3545;
  --bg:#eef3f9;--card:#fff;--muted:#64748b;--line:#dbe3ee;--ink:#0f172a;--soft:#f8fbff;
  --shadow:0 18px 40px rgba(15,59,99,.12);--shadow2:0 10px 24px rgba(15,23,42,.08);--radius:22px;
  --tap:44px;--shell:1320px;--sidebar:420px;
}
*{box-sizing:border-box}html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}body{margin:0;background:radial-gradient(circle at top left,#e9f4ff 0,#eef3f9 38%,#f7f9fc 100%);font-family:system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif;color:var(--ink);font-size:15px;min-height:100vh}.no-scroll{overflow:hidden}.container-fluid{width:100%;max-width:var(--shell);margin:0 auto;padding-left:14px;padding-right:14px}.appbar{background:linear-gradient(135deg,var(--brand2),var(--brand));color:#fff;border-radius:0 0 30px 30px;padding:18px 0 34px;box-shadow:var(--shadow);position:sticky;top:0;z-index:20}.appbar h1{letter-spacing:-.035em}.appbar-grid{display:grid;grid-template-columns:1fr auto;gap:16px;align-items:center}.toolbar{display:grid;grid-template-columns:160px minmax(240px,1fr) auto;gap:8px;align-items:center}.device-chip{display:inline-flex;align-items:center;gap:.4rem;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.12);color:#fff;border-radius:999px;padding:.28rem .65rem;font-size:.75rem;font-weight:800;margin-top:.45rem;backdrop-filter:blur(10px)}.shell{max-width:var(--shell);margin:-24px auto 40px;padding:0 14px;position:relative;z-index:2}.row{display:flex;flex-wrap:wrap;margin-left:-8px;margin-right:-8px}.row>*{padding-left:8px;padding-right:8px}.g-2{row-gap:8px}.g-3{row-gap:16px}.col-12{flex:0 0 auto;width:100%}.col-6{flex:0 0 auto;width:50%}.layout-grid{display:grid;grid-template-columns:minmax(0,1fr) var(--sidebar);gap:16px;align-items:start}.main-panel{min-width:0}.side-panel{position:sticky;top:116px;display:grid;gap:16px}.mb-0{margin-bottom:0!important}.mb-2{margin-bottom:.55rem!important}.mb-3{margin-bottom:1rem!important}.mt-3{margin-top:1rem!important}.m-0{margin:0!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-3{padding:1rem!important}.d-flex{display:flex!important}.flex-wrap{flex-wrap:wrap!important}.align-items-center{align-items:center!important}.align-items-end{align-items:flex-end!important}.justify-content-between{justify-content:space-between!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.w-100{width:100%!important}.d-none{display:none!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-muted{color:var(--muted)!important}.text-success{color:var(--ok)!important}.text-danger{color:var(--danger)!important}.fw-bold{font-weight:700!important}.fw-black{font-weight:900!important}.opacity-75{opacity:.75}.small{font-size:.85rem}.h3{font-size:1.55rem}.h5{font-size:1.15rem}.cardx{background:rgba(255,255,255,.96);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;backdrop-filter:blur(8px)}.kpi{padding:16px 18px;min-height:102px;position:relative}.kpi:after{content:"";position:absolute;inset:auto 14px 0 14px;height:3px;border-radius:999px;background:linear-gradient(90deg,var(--accent),transparent);opacity:.45}.kpi .label{color:var(--muted);font-size:.84rem;font-weight:750}.kpi .value{font-size:1.85rem;font-weight:950;line-height:1.05;color:#09203f;letter-spacing:-.04em}.small-muted{font-size:.82rem;color:var(--muted)}
.form-control,.form-select,textarea,input,select{font:inherit}.form-control,.form-select{width:100%;border:1px solid #cbd5e1;border-radius:12px;background:#fff;color:#0f172a;padding:.62rem .72rem;outline:none;min-height:var(--tap)}textarea.form-control{min-height:auto}.form-control:focus,.form-select:focus{border-color:var(--accent);box-shadow:0 0 0 4px rgba(29,117,189,.12)}.form-label{display:block;font-weight:800;margin-bottom:.25rem;color:#334155}.btn{appearance:none;border:1px solid transparent;border-radius:12px;background:#f8fafc;color:#0f172a;font-weight:850;padding:.62rem .9rem;min-height:var(--tap);cursor:pointer;line-height:1.15;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:.35rem;transition:transform .08s ease,box-shadow .12s ease,background .12s ease}.btn:hover{transform:translateY(-1px);box-shadow:var(--shadow2)}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-light{background:#fff;color:var(--brand);border-color:#fff}.btn-brand{background:var(--brand);border-color:var(--brand);color:#fff}.btn-brand:hover{background:var(--brand2);border-color:var(--brand2);color:#fff}.btn-secondary{background:#e2e8f0;border-color:#cbd5e1;color:#0f172a}.btn-outline-primary{background:#fff;border-color:#9ec5fe;color:#0d4f8b}.btn-outline-secondary{background:#fff;border-color:#cbd5e1;color:#334155}.btn-outline-dark{background:#fff;border-color:#334155;color:#0f172a}.btn-outline-danger{background:#fff;border-color:#fecaca;color:#b91c1c}.btn-sm{padding:.42rem .68rem;border-radius:10px;font-size:.83rem;min-height:38px}.btn-close{width:36px;height:36px;border:0;border-radius:999px;background:#f1f5f9;color:#334155;font-size:1.3rem;line-height:1;cursor:pointer}.btn-close:before{content:"×"}.actions-wrap{display:flex;flex-wrap:wrap;gap:.5rem}.actions-wrap .btn{white-space:nowrap}.table-responsive{width:100%;overflow-x:auto}.table{width:100%;border-collapse:separate;border-spacing:0}.table th,.table td{padding:.72rem .62rem;border-bottom:1px solid var(--line);vertical-align:middle}.table thead th{font-size:.74rem;text-transform:uppercase;color:#64748b;letter-spacing:.04em;background:#f8fafc;position:sticky;top:0;z-index:1}.table tbody tr:last-child td{border-bottom:0}.qty{font-weight:950;color:#09203f}.product-row{cursor:pointer}.product-row:hover{background:#f8fafc}.pill{border-radius:999px;padding:.35rem .7rem;background:#eef6ff;color:#0f3b63;font-weight:800;font-size:.82rem}.alert{border:1px solid transparent;border-radius:16px;padding:.85rem 3rem .85rem 1rem;margin-bottom:1rem;position:relative;box-shadow:var(--shadow)}.alert-success{background:#ecfdf5;border-color:#bbf7d0;color:#065f46}.alert-danger{background:#fef2f2;border-color:#fecaca;color:#991b1b}.alert .btn-close{position:absolute;right:.65rem;top:.5rem;background:transparent}.modal{position:fixed;inset:0;background:rgba(2,6,23,.58);display:none;z-index:1000;align-items:center;justify-content:center;padding:18px}.modal.show{display:flex}.modal-dialog{width:min(860px,100%);max-height:92vh}.modal-content{background:#fff;border-radius:22px;box-shadow:0 25px 80px rgba(0,0,0,.28);overflow:hidden;max-height:92vh;display:flex;flex-direction:column}.modal-header,.modal-footer{padding:1rem;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:.75rem}.modal-footer{border-top:1px solid var(--line);border-bottom:0;justify-content:flex-end}.modal-title{margin:0;font-size:1.15rem;font-weight:900}.modal-body{padding:1rem;overflow:auto}.desktop-only{display:initial}.mobile-only{display:none}
@media (min-width:768px){.col-md-2{flex:0 0 auto;width:16.6667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.3333%}.col-md-7{flex:0 0 auto;width:58.3333%}}
@media (min-width:1200px){.col-xl-4{flex:0 0 auto;width:33.3333%}.col-xl-8{flex:0 0 auto;width:66.6667%}}
@media (min-width:1400px){:root{--shell:1480px;--sidebar:450px}.kpi .value{font-size:2.05rem}.shell{padding-left:18px;padding-right:18px}.cardx.p-3{padding:1.15rem!important}}
@media (min-width:768px) and (max-width:1199px){:root{--shell:100%;--sidebar:360px}.appbar{position:sticky}.appbar-grid{grid-template-columns:1fr}.toolbar{grid-template-columns:1fr 1fr auto}.layout-grid{grid-template-columns:1fr}.side-panel{position:static;grid-template-columns:1fr 1fr}.side-panel .cardx:first-child{grid-column:1/-1}.kpi{min-height:96px}.table{min-width:760px}.device-chip:after{content:" · modo tablet"}}
@media (max-width:767px){:root{--tap:48px;--radius:18px}.desktop-only{display:none!important}.mobile-only{display:initial}.appbar{position:sticky;border-radius:0 0 22px 22px;padding:12px 0 24px}.appbar-grid{grid-template-columns:1fr;gap:12px}.toolbar{grid-template-columns:1fr;gap:8px}.toolbar .btn{width:100%}.shell{margin-top:-16px;padding-left:10px;padding-right:10px}.h3{font-size:1.25rem}.h5{font-size:1.05rem}body{font-size:14px}.row{margin-left:-5px;margin-right:-5px}.row>*{padding-left:5px;padding-right:5px}.g-3{row-gap:10px}.col-6{width:50%}.kpi{padding:12px;min-height:86px;border-radius:16px}.kpi .label{font-size:.72rem}.kpi .value{font-size:1.33rem}.kpi .small-muted{font-size:.72rem}.layout-grid{display:block}.side-panel{position:static;margin-top:10px;display:block}.side-panel .cardx{margin-bottom:10px}.cardx{border-radius:18px}.cardx.p-3{padding:.82rem!important}.actions-wrap{display:grid;grid-template-columns:1fr 1fr;width:100%}.actions-wrap .btn{width:100%;min-height:42px}.table-responsive{overflow:visible}.stock-table,.summary-table{display:block}.stock-table thead,.summary-table thead{display:none}.stock-table tbody,.summary-table tbody{display:grid;gap:10px}.stock-table tr,.summary-table tr{display:block;background:#fff;border:1px solid var(--line);border-radius:16px;box-shadow:0 8px 18px rgba(15,23,42,.06);padding:10px}.stock-table td,.summary-table td{display:flex;justify-content:space-between;gap:12px;border-bottom:1px dashed #e2e8f0;padding:.45rem .15rem!important;text-align:right!important}.stock-table td:last-child,.summary-table td:last-child{border-bottom:0}.stock-table td:before,.summary-table td:before{font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:#64748b;font-weight:850;text-align:left}.stock-table td:nth-child(1),.summary-table td:nth-child(1){display:block;text-align:left!important}.stock-table td:nth-child(1):before,.summary-table td:nth-child(1):before{content:""}.stock-table td:nth-child(2):before{content:"Piezas"}.stock-table td:nth-child(3):before{content:"Paquetes"}.stock-table td:nth-child(4):before{content:"Tarimas"}.stock-table td:nth-child(5){justify-content:stretch}.stock-table td:nth-child(5):before{content:""}.stock-table td:nth-child(5) .btn{width:100%}.summary-table td:nth-child(2):before{content:"Inicial 07:00"}.summary-table td:nth-child(3):before{content:"Entradas"}.summary-table td:nth-child(4):before{content:"Salidas"}.summary-table td:nth-child(5):before{content:"Actual"}.modal{padding:0;align-items:flex-end}.modal-dialog{width:100%;max-height:94vh}.modal-content{border-radius:22px 22px 0 0;max-height:94vh}.modal-footer{display:grid;grid-template-columns:1fr 1fr}.modal-footer .btn{width:100%}.item-row{display:grid!important;grid-template-columns:1fr;gap:8px}.item-row [class*="col-"]{width:100%;padding-left:0;padding-right:0}.device-chip:after{content:" · modo móvil"}}
@media (max-width:420px){.col-6{width:100%}.kpi{min-height:auto}.actions-wrap{grid-template-columns:1fr}.modal-footer{grid-template-columns:1fr}.btn{width:100%}}
@media (hover:none) and (pointer:coarse){.btn:hover{transform:none;box-shadow:none}.product-row:hover{background:#fff}.btn,.form-control,.form-select{min-height:50px}}

/* ============================================================
   CABECERO COMPACTO ADAPTATIVO
   - PC: barra baja y controles en una sola línea.
   - Tablet: controles compactos en grid.
   - Celular: cabecero mínimo, sin invadir KPIs.
============================================================ */
.appbar{padding:10px 0 14px!important;border-radius:0 0 18px 18px!important;box-shadow:0 10px 26px rgba(15,59,99,.16)!important;}
.appbar-grid{grid-template-columns:minmax(260px,1fr) minmax(460px,760px)!important;gap:12px!important;}
.appbar .small{font-size:.78rem!important;line-height:1.15!important;}
.appbar .h3{font-size:1.28rem!important;line-height:1.08!important;}
.device-chip{margin-top:.25rem!important;padding:.18rem .55rem!important;font-size:.68rem!important;}
.toolbar{grid-template-columns:150px minmax(260px,1fr) 110px!important;gap:7px!important;}
.toolbar .form-control,.toolbar .form-select,.toolbar .btn{min-height:38px!important;border-radius:11px!important;padding:.45rem .65rem!important;}
.shell{margin:12px auto 36px!important;}
.side-panel{top:82px!important;}
.kpi{min-height:86px!important;padding:12px 16px!important;}
.kpi .value{font-size:1.55rem!important;}
.kpi .label{font-size:.78rem!important;}
@media (min-width:1200px){
  .appbar{position:sticky!important;}
  .appbar-grid{align-items:center!important;}
}
@media (min-width:768px) and (max-width:1199px){
  .appbar{padding:9px 0 12px!important;border-radius:0 0 16px 16px!important;}
  .appbar-grid{grid-template-columns:1fr!important;gap:8px!important;}
  .toolbar{grid-template-columns:150px minmax(220px,1fr) 110px!important;}
  .shell{margin-top:10px!important;}
  .side-panel{top:auto!important;}
  .kpi{min-height:78px!important;padding:11px 14px!important;}
  .kpi .value{font-size:1.42rem!important;}
}
@media (max-width:767px){
  .appbar{padding:8px 0 10px!important;border-radius:0 0 14px 14px!important;position:sticky!important;}
  .appbar-grid{grid-template-columns:1fr!important;gap:8px!important;}
  .appbar .small:first-child{display:none!important;}
  .appbar .h3{font-size:1.06rem!important;}
  .appbar .small.opacity-75:not(:first-child){font-size:.72rem!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;}
  .device-chip{display:none!important;}
  .toolbar{grid-template-columns:1fr 1fr!important;gap:6px!important;}
  .toolbar .form-control,.toolbar .form-select,.toolbar .btn{min-height:40px!important;border-radius:10px!important;font-size:.86rem!important;padding:.42rem .55rem!important;}
  .toolbar .form-select{grid-column:1 / -1!important;}
  .toolbar .btn{width:100%!important;}
  .shell{margin-top:8px!important;padding-left:8px!important;padding-right:8px!important;}
  .kpi{min-height:72px!important;padding:10px 11px!important;}
  .kpi .value{font-size:1.18rem!important;}
  .kpi .label{font-size:.68rem!important;}
}
@media (max-width:420px){
  .toolbar{grid-template-columns:1fr!important;}
  .toolbar .form-select{grid-column:auto!important;}
  .appbar .h3{font-size:1rem!important;}
}

/* ============================================================
   PANEL DE BOTONES PARA REPARTIDOR
============================================================ */
.action-shell{margin-top:8px;}
.action-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;}
.action-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:14px;}
.action-tile{border:1px solid var(--line);background:linear-gradient(180deg,#fff,#f8fbff);border-radius:20px;padding:18px 14px;min-height:150px;text-align:left;cursor:pointer;box-shadow:var(--shadow2);display:flex;flex-direction:column;gap:8px;color:var(--ink);transition:transform .12s ease, box-shadow .12s ease, border-color .12s ease;}
.action-tile:hover{transform:translateY(-2px);box-shadow:var(--shadow);border-color:#9ec5fe;}
.action-tile.primary{background:linear-gradient(135deg,var(--brand),var(--accent));color:#fff;border-color:transparent;}
.action-tile.danger-soft{background:linear-gradient(180deg,#fff,#fff7ed);border-color:#fed7aa;}
.tile-icon{font-size:1.8rem;line-height:1;}
.tile-title{font-size:1.05rem;font-weight:950;letter-spacing:-.02em;}
.tile-text{font-size:.84rem;color:var(--muted);line-height:1.25;}
.action-tile.primary .tile-text{color:rgba(255,255,255,.86);}
.modal-wide{width:min(1040px,100%);}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
.checkline{display:flex;align-items:flex-start;gap:10px;font-weight:850;color:#0f172a;}
.checkline input{width:20px;height:20px;accent-color:var(--brand);margin-top:2px;}
.close-warning{border:1px solid #fed7aa;background:#fff7ed;color:#9a3412;border-radius:16px;padding:12px;font-size:.9rem;font-weight:750;}
.item-row{background:#f8fafc;border:1px solid var(--line);border-radius:16px;padding:10px;margin-bottom:10px;}
@media (max-width:1199px){.action-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.action-tile{min-height:132px}.action-tile:last-child{grid-column:1/-1;}}
@media (max-width:767px){.action-head{align-items:flex-start;flex-direction:column}.action-grid{grid-template-columns:1fr;gap:10px}.action-tile{min-height:96px;padding:14px;display:grid;grid-template-columns:auto 1fr;grid-template-areas:"icon title" "icon text";column-gap:12px}.tile-icon{grid-area:icon;font-size:1.7rem}.tile-title{grid-area:title}.tile-text{grid-area:text}.form-grid{grid-template-columns:1fr}.modal-wide{width:100%;}.action-shell{padding:.85rem!important;}}


.readonly-ref{min-height:44px;display:flex;align-items:center;padding:0 14px;border:1px dashed #9bbbe8;border-radius:14px;background:#f8fbff;color:#16456f;font-weight:800;letter-spacing:.2px;}
.calc-preview{border:1px solid #dbeafe;background:#f8fbff;border-radius:12px;padding:.55rem .7rem;font-size:.82rem;color:#16456f;font-weight:800;min-height:44px;display:flex;align-items:center;}
.summary-totals{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px}.summary-total-card{border:1px solid var(--line);border-radius:16px;background:#f8fbff;padding:10px 12px}.summary-total-card .label{font-size:.72rem;color:#64748b;font-weight:800;text-transform:uppercase}.summary-total-card .value{font-size:1.35rem;font-weight:950;color:#08264d}.delivery-row{grid-template-columns:1.3fr .65fr .55fr .85fr .45fr!important;}
@media (max-width:767px){.summary-totals{grid-template-columns:1fr}.delivery-row{grid-template-columns:1fr!important;}}


/* ============================================================
   REGISTRAR SALIDA · DISEÑO VISUAL PARA REPARTIDOR
============================================================ */
.delivery-hero{display:flex;gap:12px;align-items:flex-start;background:linear-gradient(135deg,#eef7ff,#f8fbff);border:1px solid #cfe2ff;border-radius:18px;padding:12px 14px;margin-bottom:14px;}
.delivery-hero-icon{width:42px;height:42px;border-radius:16px;background:linear-gradient(135deg,#0f3b63,#1d75bd);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.35rem;flex:0 0 auto;box-shadow:0 10px 20px rgba(15,59,99,.18)}
.delivery-hero-title{font-weight:950;color:#08264a;font-size:1rem;line-height:1.1}.delivery-hero-text{color:#64748b;font-size:.84rem;margin-top:2px;line-height:1.25}.delivery-form-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:12px;margin-bottom:14px}.delivery-field{background:#fff;border:1px solid #dbe5f1;border-radius:18px;padding:12px;box-shadow:0 8px 18px rgba(15,23,42,.04)}.delivery-field .form-label{display:flex;align-items:center;gap:7px;margin-bottom:8px}.delivery-section-title{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:10px 0 12px}.delivery-section-title .title-left{display:flex;align-items:center;gap:10px}.delivery-section-title .bubble{width:36px;height:36px;border-radius:14px;background:#eef6ff;color:#0f3b63;display:flex;align-items:center;justify-content:center;font-size:1.15rem;border:1px solid #cfe2ff}.btn-add-line{background:#ecfdf5!important;border-color:#bbf7d0!important;color:#065f46!important;font-weight:950!important}.btn-add-line:before{content:'＋';font-weight:950}.delivery-list{display:grid;gap:12px}.delivery-card-row{background:linear-gradient(180deg,#fff,#fbfdff)!important;border:1px solid #cfe0f3!important;border-radius:20px!important;padding:14px!important;margin-bottom:0!important;box-shadow:0 12px 28px rgba(15,59,99,.08)}.delivery-row-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}.delivery-row-title{display:flex;align-items:center;gap:10px;min-width:0}.delivery-row-title .icon{width:38px;height:38px;border-radius:14px;background:#fff7ed;color:#9a3412;border:1px solid #fed7aa;display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex:0 0 auto}.delivery-row-title strong{display:block;color:#08264a;line-height:1.1}.delivery-row-title small{display:block;color:#64748b;font-size:.78rem;margin-top:2px}.btn-remove-icon{width:42px;min-width:42px;height:42px;border-radius:14px!important;background:#fff!important;border:1px solid #fecaca!important;color:#b91c1c!important;font-size:1.05rem!important;padding:0!important}.delivery-row-grid{display:grid;grid-template-columns:minmax(220px,1.35fr) minmax(150px,.65fr) minmax(150px,.65fr) minmax(170px,.8fr);gap:10px;align-items:end}.field-box{background:#f8fbff;border:1px solid #dbeafe;border-radius:16px;padding:10px}.field-box label{display:flex;align-items:center;gap:7px;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;color:#47637f;font-weight:950;margin-bottom:7px}.field-box .form-control,.field-box .form-select{background:#fff;border-radius:13px}.qty-stepper{display:grid;grid-template-columns:42px 1fr 42px;gap:6px;align-items:center}.step-btn{width:42px;height:42px;border-radius:13px;border:1px solid #bfdbfe;background:#eef6ff;color:#0f3b63;font-weight:950;font-size:1.25rem;cursor:pointer}.step-btn.minus{background:#fff7ed;border-color:#fed7aa;color:#9a3412}.step-btn:hover{filter:brightness(.98)}.qty-hint{display:block;margin-top:6px;color:#64748b;font-size:.74rem;font-weight:750}.calc-preview{gap:9px;justify-content:flex-start}.calc-preview .calc-icon{width:34px;height:34px;border-radius:13px;background:#eef6ff;color:#0f3b63;display:flex;align-items:center;justify-content:center;flex:0 0 auto}.calc-preview .calc-main{font-weight:950;color:#08264a}.calc-preview .calc-sub{font-size:.72rem;color:#64748b;font-weight:750}.calc-preview.warn{border-color:#fed7aa!important;background:#fff7ed!important;color:#9a3412!important}.calc-preview.danger{border-color:#fecaca!important;background:#fef2f2!important;color:#991b1b!important}.empty-delivery{border:1px dashed #cbd5e1;border-radius:18px;padding:18px;text-align:center;color:#64748b;background:#f8fafc}.modal-footer .btn-success-route{background:linear-gradient(135deg,#0f3b63,#1d75bd);color:#fff;border:0;font-weight:950}.modal-footer .btn-cancel-soft{background:#f1f5f9;border-color:#cbd5e1;color:#334155}.out-notes-box{background:#fff;border:1px solid #dbe5f1;border-radius:18px;padding:12px;margin-top:14px}.out-notes-box .form-label{display:flex;align-items:center;gap:7px}
.customer-input-row{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center}.btn-camera-customer{min-width:52px;height:48px;border-radius:15px!important;background:linear-gradient(135deg,#0f3b63,#1d75bd)!important;color:#fff!important;border:0!important;font-size:1.25rem!important;box-shadow:0 10px 22px rgba(15,59,99,.18)}.customer-scan-help{margin-top:8px;background:#f8fbff;border:1px dashed #bfdbfe;border-radius:14px;padding:8px 10px;color:#47637f;font-size:.78rem}.camera-scan-box{background:#020617;border-radius:18px;overflow:hidden;position:relative;border:1px solid #1e293b;min-height:260px;display:flex;align-items:center;justify-content:center}.camera-scan-box video{width:100%;height:min(56vh,420px);object-fit:cover;background:#020617}.scan-frame{position:absolute;inset:18%;border:3px solid rgba(34,197,94,.9);border-radius:18px;box-shadow:0 0 0 999px rgba(2,6,23,.28)}.scan-frame:after{content:'Apunta al QR/código del cliente';position:absolute;left:50%;bottom:-38px;transform:translateX(-50%);white-space:nowrap;background:rgba(15,23,42,.84);color:#fff;border-radius:999px;padding:7px 12px;font-weight:850;font-size:.8rem}.scan-status{background:#f8fbff;border:1px solid #dbeafe;border-radius:14px;padding:10px 12px;color:#334155;font-weight:750}.scan-status.ok{background:#ecfdf5;border-color:#bbf7d0;color:#065f46}.scan-status.warn{background:#fff7ed;border-color:#fed7aa;color:#9a3412}.manual-code-row{display:grid;grid-template-columns:1fr auto;gap:8px}.customer-picked{margin-top:8px;background:#ecfdf5;border:1px solid #bbf7d0;border-radius:14px;padding:8px 10px;color:#065f46;font-weight:850}
@media (max-width:991px){.delivery-form-grid{grid-template-columns:1fr}.delivery-row-grid{grid-template-columns:1fr 1fr}.field-product{grid-column:1/-1}.calc-preview{grid-column:1/-1}}@media (max-width:767px){.delivery-hero{padding:10px;border-radius:16px}.delivery-hero-icon{width:38px;height:38px;border-radius:14px}.delivery-row-grid{grid-template-columns:1fr}.delivery-card-row{padding:12px!important;border-radius:18px!important}.field-box{padding:9px}.qty-stepper{grid-template-columns:46px 1fr 46px}.step-btn{width:46px;height:46px}.delivery-section-title{align-items:flex-start;flex-direction:column}.btn-add-line{width:100%}.modal-footer .btn-success-route,.modal-footer .btn-cancel-soft{width:100%}}


/* ============================================================
   BOTÓN FLOTANTE INTELIGENTE · ESTADO DE RUTA
============================================================ */
.smart-float{
  position:fixed;right:18px;bottom:18px;z-index:800;
  border:0;border-radius:22px;padding:12px 16px;
  min-width:260px;max-width:360px;text-align:left;color:#fff;cursor:pointer;
  background:linear-gradient(135deg,#08264a,#1368a9 58%,#18a4d8);
  box-shadow:0 18px 42px rgba(8,38,74,.32);
  display:flex;gap:12px;align-items:center;
}
.smart-float:hover{transform:translateY(-2px);box-shadow:0 22px 52px rgba(8,38,74,.38)}
.smart-float .smart-icon{width:42px;height:42px;border-radius:16px;background:rgba(255,255,255,.18);display:flex;align-items:center;justify-content:center;font-size:22px;flex:0 0 auto;border:1px solid rgba(255,255,255,.20)}
.smart-float .smart-title{font-weight:950;font-size:.92rem;line-height:1.05}
.smart-float .smart-sub{font-size:.76rem;opacity:.86;line-height:1.15;margin-top:2px}
.smart-float .smart-badge{margin-left:auto;background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.18);border-radius:999px;padding:.22rem .5rem;font-weight:900;font-size:.72rem}
.smart-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:12px}
.smart-card{background:#f8fbff;border:1px solid #dbeafe;border-radius:16px;padding:12px}
.smart-card .label{color:#64748b;font-size:.76rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em}
.smart-card .value{font-size:1.25rem;font-weight:950;color:#08264a;line-height:1.05}
.smart-notice{border-radius:16px;padding:12px;background:#fff7ed;border:1px solid #fed7aa;color:#9a3412;margin-top:12px}
.smart-ok{background:#ecfdf5;border-color:#bbf7d0;color:#065f46}
.smart-list{display:grid;gap:8px}
.smart-item{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:8px;align-items:center;padding:10px;border:1px solid #e2e8f0;border-radius:14px;background:#fff}
.smart-item .name{min-width:0}
.smart-item .sku{font-weight:900;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.smart-item .desc{font-size:.76rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.smart-chip{border-radius:999px;padding:.25rem .55rem;font-size:.74rem;font-weight:900;background:#eef6ff;color:#0f3b63}
.smart-chip.warn{background:#fff7ed;color:#9a3412}
@media(max-width:767px){
  .smart-float{left:10px;right:10px;bottom:10px;max-width:none;min-width:0;border-radius:18px;padding:11px 12px}
  .smart-float .smart-icon{width:38px;height:38px}
  .smart-grid{grid-template-columns:1fr}
  .smart-item{grid-template-columns:1fr 1fr;gap:6px}
  .smart-item .name{grid-column:1/-1}
}


/* ============================================================
   AJUSTE REPARTO · CABECERA MÓVIL/TABLET COMPACTA
   - Se elimina KPI duplicado del cabecero; se conserva card grande.
   - En móvil: fecha, consultar y salir quedan en una sola línea.
============================================================ */
.toolbar-modern{
  grid-template-columns:145px minmax(220px,1fr) 104px 86px!important;
  align-items:center!important;
}
.logout-btn{
  background:linear-gradient(135deg,#fff7ed,#ffe4e6)!important;
  border:1px solid rgba(248,113,113,.55)!important;
  color:#8a1f1f!important;
  font-weight:950!important;
  box-shadow:0 8px 18px rgba(127,29,29,.10)!important;
}
.logout-btn:hover{background:linear-gradient(135deg,#fee2e2,#fff)!important;color:#7f1d1d!important;}
.logout-icon{font-weight:950;line-height:1;font-size:1rem;}
.logout-text{font-weight:950;}
@media (min-width:768px) and (max-width:1199px){
  .appbar-modern{padding:8px 0 10px!important;border-radius:0 0 16px 16px!important;}
  .appbar-grid-modern{grid-template-columns:1fr!important;gap:8px!important;}
  .brand-block{gap:1px!important;}
  .brand-eyebrow{font-size:.68rem!important;}
  .appbar .h3{font-size:1.12rem!important;}
  .brand-meta{font-size:.72rem!important;}
  .route-mini{margin-top:3px!important;padding:3px 8px!important;}
  .toolbar-modern{grid-template-columns:150px minmax(250px,1fr) 102px 82px!important;gap:6px!important;}
  .toolbar-modern .form-control,.toolbar-modern .form-select,.toolbar-modern .btn{min-height:36px!important;font-size:.82rem!important;padding:.38rem .55rem!important;}
}
@media (max-width:767px){
  .appbar-modern{padding:7px 0 9px!important;border-radius:0 0 14px 14px!important;}
  .appbar-grid-modern{gap:7px!important;}
  .brand-block{gap:0!important;}
  .brand-eyebrow{display:none!important;}
  .appbar .h3{font-size:1.02rem!important;line-height:1.05!important;}
  .brand-meta{font-size:.70rem!important;line-height:1.05!important;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
  .route-mini{width:auto!important;max-width:100%!important;margin-top:4px!important;padding:3px 8px!important;gap:5px!important;}
  .route-mini-label{display:none!important;}
  .route-mini strong{font-size:.72rem!important;max-width:100%!important;}
  .toolbar-modern{grid-template-columns:minmax(130px,1fr) 98px 54px!important;gap:6px!important;}
  .toolbar-modern #routeSelect:not(.d-none){grid-column:1 / -1!important;}
  .toolbar-modern .form-control,.toolbar-modern .form-select,.toolbar-modern .btn{min-height:38px!important;border-radius:12px!important;font-size:.82rem!important;padding:.38rem .48rem!important;}
  .toolbar-modern .btn-light{font-size:.82rem!important;font-weight:950!important;}
  .logout-btn{min-width:54px!important;padding:.36rem .45rem!important;border-radius:12px!important;}
  .logout-icon{display:none!important;}
  .logout-text{font-size:.80rem!important;}
  .shell{margin-top:10px!important;}
}
@media (max-width:420px){
  .toolbar-modern{grid-template-columns:minmax(124px,1fr) 92px 52px!important;}
  .toolbar-modern .form-control,.toolbar-modern .form-select,.toolbar-modern .btn{font-size:.78rem!important;}
  .appbar .h3{font-size:.98rem!important;}
}
</style>
</head>
<body>
<header class="appbar appbar-modern">
  <div class="container-fluid">
    <div class="appbar-grid appbar-grid-modern">
      <div class="brand-block">
        <div class="brand-eyebrow">BReport · Planta Botella</div>
        <h1 class="h3 m-0 fw-black">Repartidor / Reparto</h1>
        <div class="brand-meta"><?=htmlspecialchars($scopeCompanyName)?> · <?=htmlspecialchars($scopeLocationName)?></div>
        <div class="route-mini" id="headerRouteMini">
          <span class="route-mini-label">Ruta activa</span>
          <strong id="headerRouteText">Consultando...</strong>
        </div>
      </div>
      <div class="toolbar toolbar-modern">
        <input type="date" id="workDate" class="form-control" value="<?=htmlspecialchars($defaultWorkDate)?>">
        <select id="routeSelect" class="form-select d-none"></select>
        <button class="btn btn-light" onclick="loadAll()">Consultar</button>
        <a class="btn nav-btn logout-btn" href="logout.php" aria-label="Salir"><span class="logout-icon">⎋</span><span class="logout-text">Salir</span></a>
      </div>
    </div>
  </div>
</header>

<main class="shell">
  <div id="alertBox"></div>

  <!-- KPIs ocultos en pantalla principal: se muestran desde el botón KPI / Totales. -->


  <section class="action-shell cardx p-3">
    <div class="action-head">
      <div>
        <div class="small-muted">Panel rápido de reparto</div>
        <h2 class="h5 mb-0">¿Qué necesitas hacer?</h2>
      </div>
      <div class="pill" id="quickRoutePill">Ruta activa</div>
    </div>

    <div class="action-grid">
      <button class="action-tile kpi-tile" type="button" onclick="openKpiModal()">
        <span class="tile-icon">📈</span>
        <span class="tile-title">KPI / Totales</span>
        <span class="tile-text">Ver ruta, inventario, entradas, salidas y productos.</span>
      </button>
      <button class="action-tile" type="button" onclick="openInventory()">
        <span class="tile-icon">📦</span>
        <span class="tile-title">Inventario de ruta</span>
        <span class="tile-text">Ver existencias cargadas por artículo.</span>
      </button>
      <button class="action-tile primary" type="button" onclick="openDelivery()">
        <span class="tile-icon">🧾</span>
        <span class="tile-title">Registrar salida</span>
        <span class="tile-text">Entregar varios productos a un cliente.</span>
      </button>
      <button class="action-tile" type="button" onclick="openSummary()">
        <span class="tile-icon">📊</span>
        <span class="tile-title">Resumen operativo</span>
        <span class="tile-text">Inicial, entradas, salidas y actual.</span>
      </button>
      <button class="action-tile" type="button" onclick="openReload()">
        <span class="tile-icon">🔄</span>
        <span class="tile-title">Solicitar recarga</span>
        <span class="tile-text">Pedir más producto a almacén.</span>
      </button>
      <button class="action-tile danger-soft" type="button" onclick="openCloseDay()">
        <span class="tile-icon">✅</span>
        <span class="tile-title">Cierre del día</span>
        <span class="tile-text">Confirmar producto con el que regresa la ruta.</span>
      </button>
    </div>
  </section>
</main>

<div class="modal" id="kpiModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">KPI / Totales de ruta</h5><div class="small-muted">Indicadores rápidos del día operativo y ruta asignada.</div></div><button class="btn-close" type="button" onclick="closeModal('kpiModal')" aria-label="Cerrar"></button></div><div class="modal-body"><section class="row g-3 kpi-grid kpi-grid-modal"><div class="col-12 col-md-4"><div class="cardx kpi"><div class="label">Ruta asignada</div><div class="value" id="kRoute">-</div><div class="small-muted" id="kRouteName">-</div></div></div><div class="col-6 col-md-2"><div class="cardx kpi"><div class="label">Inventario actual</div><div class="value" id="kStock">0</div><div class="small-muted">botellas/piezas</div></div></div><div class="col-6 col-md-2"><div class="cardx kpi"><div class="label">Entradas del día</div><div class="value" id="kIn">0</div><div class="small-muted">desde 07:00</div></div></div><div class="col-6 col-md-2"><div class="cardx kpi"><div class="label">Salidas del día</div><div class="value" id="kOut">0</div><div class="small-muted">entregas/devoluciones</div></div></div><div class="col-6 col-md-2"><div class="cardx kpi"><div class="label">Productos</div><div class="value" id="kProducts">0</div><div class="small-muted">con saldo</div></div></div></section></div><div class="modal-footer"><button class="btn btn-secondary" type="button" onclick="closeModal('kpiModal')">Cerrar</button><button class="btn btn-brand" type="button" onclick="openSummary();closeModal('kpiModal')">Ver resumen operativo</button></div></div></div></div>

<div class="modal" id="inventoryModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Inventario de ruta</h5><div class="small-muted">Existencias actuales cargadas en el almacén móvil.</div></div><button class="btn-close" type="button" onclick="closeModal('inventoryModal')" aria-label="Cerrar"></button></div><div class="modal-body"><div class="table-responsive"><table class="table stock-table"><thead><tr><th>Artículo</th><th class="text-end">Piezas</th><th class="text-end">Paquetes</th><th class="text-end">Tarimas</th></tr></thead><tbody id="stockBody"><tr><td colspan="4" class="text-center py-4 text-muted">Consulta para ver inventario.</td></tr></tbody></table></div></div><div class="modal-footer"><button class="btn btn-secondary" type="button" onclick="closeModal('inventoryModal')">Cerrar</button><button class="btn btn-brand" type="button" onclick="openDelivery();closeModal('inventoryModal')">Registrar salida</button></div></div></div></div>

<div class="modal" id="deliveryModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Registrar salida a cliente</h5><div class="small-muted">Captura la visita completa: cliente, productos y cantidades entregadas.</div></div><button class="btn-close" type="button" onclick="closeModal('deliveryModal')" aria-label="Cerrar"></button></div><div class="modal-body"><div class="delivery-hero"><div class="delivery-hero-icon">🧾</div><div><div class="delivery-hero-title">Entrega desde la ruta activa</div><div class="delivery-hero-text">El sistema convierte tarimas, paquetes o piezas a botellas/piezas y descuenta únicamente del inventario de la ruta.</div></div></div><div class="delivery-form-grid"><div class="delivery-field"><label class="form-label">🏪 Cliente / destino</label><div class="customer-input-row"><input id="outCustomer" class="form-control" list="customersList" placeholder="Escribe, selecciona o escanea cliente"><button class="btn btn-camera-customer" type="button" onclick="openCustomerCameraScanner()" title="Escanear código de cliente">📷</button></div><datalist id="customersList"></datalist><div class="customer-scan-help">Puedes escribir el cliente, elegirlo de la lista o escanear el código pegado en el negocio.</div><div id="customerScanResult" class="customer-picked d-none"></div></div><div class="delivery-field"><label class="form-label">🎫 Referencia</label><div class="readonly-ref" id="outRefPreview">Se generará automáticamente al registrar</div><div class="small-muted mt-1">El folio lo asigna el sistema para evitar duplicados.</div></div></div><div class="delivery-section-title"><div class="title-left"><div class="bubble">📦</div><div><label class="form-label m-0">Productos entregados</label><div class="small-muted">Agrega uno o varios artículos de la misma visita.</div></div></div><button class="btn btn-sm btn-add-line" type="button" onclick="addDeliveryRow()"> Agregar producto</button></div><div id="deliveryItems" class="delivery-list"></div><div class="out-notes-box"><label class="form-label">📝 Notas</label><textarea id="outNotes" class="form-control" rows="2" placeholder="Comentarios de entrega, recibido por, observaciones, etc."></textarea></div></div><div class="modal-footer"><button class="btn btn-cancel-soft" type="button" onclick="closeModal('deliveryModal')">Cancelar</button><button class="btn btn-success-route" type="button" onclick="saveRouteOut()">🧾 Revisar y registrar salida</button></div></div></div></div>



<div class="modal" id="customerCameraModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Escanear cliente</h5><div class="small-muted">Usa la cámara del dispositivo para leer el código pegado en el negocio.</div></div><button class="btn-close" type="button" onclick="closeCustomerCameraScanner()" aria-label="Cerrar"></button></div><div class="modal-body"><div class="scan-status" id="customerScanStatus">Abre la cámara y apunta al QR/código del cliente.</div><div class="camera-scan-box mt-3"><video id="customerScanVideo" playsinline muted></video><div class="scan-frame"></div></div><div class="manual-code-row mt-3"><input id="manualCustomerCode" class="form-control" placeholder="También puedes capturar el código manualmente"><button class="btn btn-brand" type="button" onclick="resolveCustomerCodeFromManual()">Buscar</button></div><div class="small-muted mt-2">Requiere HTTPS para usar cámara. Si el navegador no permite lectura, captura el código manualmente.</div></div><div class="modal-footer"><button class="btn btn-secondary" type="button" onclick="closeCustomerCameraScanner()">Cerrar</button></div></div></div></div>
<div class="modal" id="confirmDeliveryModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Confirmar salida</h5><div class="small-muted">Revisa las cantidades antes de descontar inventario de la ruta.</div></div><button class="btn-close" type="button" onclick="closeModal('confirmDeliveryModal')" aria-label="Cerrar"></button></div><div class="modal-body"><div class="confirm-box"><div><span class="small-muted">Cliente / destino</span><strong id="confirmCustomer">-</strong></div><div><span class="small-muted">Referencia</span><strong>Sistema automático</strong></div></div><div class="table-responsive mt-3"><table class="table"><thead><tr><th>Artículo</th><th class="text-end">Unidad</th><th class="text-end">Cantidad</th><th class="text-end">Piezas</th></tr></thead><tbody id="confirmDeliveryBody"></tbody></table></div><div class="confirm-total"><span>Total a entregar</span><strong id="confirmTotalPieces">0 piezas</strong></div><div class="small-muted mt-2">Al confirmar se generará la salida, se descontará de la ruta y se abrirá el ticket térmico para imprimir.</div></div><div class="modal-footer"><button class="btn btn-secondary" type="button" onclick="closeModal('confirmDeliveryModal')">Regresar</button><button class="btn btn-brand" type="button" onclick="confirmAndSaveRouteOut()">Confirmar y generar ticket</button></div></div></div></div>

<div class="modal" id="summaryModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Resumen operativo del día</h5><div class="small-muted">Corte operativo de 07:00 AM a 06:59 AM.</div></div><button class="btn-close" type="button" onclick="closeModal('summaryModal')" aria-label="Cerrar"></button></div><div class="modal-body"><div class="summary-totals"><div class="summary-total-card"><div class="label">Total piezas actual</div><div class="value" id="sumTotalPieces">0</div></div><div class="summary-total-card"><div class="label">Total paquetes actual</div><div class="value" id="sumTotalPackages">0</div></div><div class="summary-total-card"><div class="label">Total tarimas actual</div><div class="value" id="sumTotalPallets">0</div></div></div><div class="table-responsive"><table class="table summary-table"><thead><tr><th>Artículo</th><th class="text-end">Inicial 07:00</th><th class="text-end">Entradas</th><th class="text-end">Salidas</th><th class="text-end">Actual piezas</th><th class="text-end">Paq. actual</th><th class="text-end">Tar. actual</th></tr></thead><tbody id="summaryBody"></tbody></table></div></div><div class="modal-footer"><button class="btn btn-secondary" type="button" onclick="closeModal('summaryModal')">Cerrar</button></div></div></div></div>

<div class="modal" id="reloadModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Solicitud de recarga</h5><div class="small-muted">Pide cualquier artículo activo de Planta Botella, aunque no venga cargado en tu ruta.</div></div><button class="btn-close" type="button" onclick="closeModal('reloadModal')" aria-label="Cerrar"></button></div><div class="modal-body"><div class="reload-help">💡 Si un cliente solicita un producto que no traes cargado, agréguelo aquí para que Almacén prepare la recarga y puedas volver más tarde.</div><div id="reloadItems"></div><button class="btn btn-sm btn-outline-primary" type="button" onclick="addReloadRow()">＋ Agregar otro artículo</button><textarea id="reloadNotes" class="form-control mt-3" rows="2" placeholder="Notas para almacén"></textarea></div><div class="modal-footer"><button class="btn btn-secondary" type="button" onclick="closeModal('reloadModal')">Cancelar</button><button class="btn btn-brand" type="button" onclick="saveReload()">Enviar solicitud</button></div></div></div></div>

<div class="modal" id="closeDayModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Cierre del día</h5><div class="small-muted">Confirma las cantidades con las que la ruta regresa a planta.</div></div><button class="btn-close" type="button" onclick="closeModal('closeDayModal')" aria-label="Cerrar"></button></div><div class="modal-body"><div class="close-warning">Revisa el inventario actual antes de cerrar. Si necesitas devolver producto a ALM01, realiza la devolución desde Almacén o el flujo correspondiente antes de confirmar cierre.</div><div class="table-responsive mt-3"><table class="table summary-table"><thead><tr><th>Artículo</th><th class="text-end">Cantidad que regresa</th><th class="text-end">Paquetes</th><th class="text-end">Tarimas</th></tr></thead><tbody id="closeSummaryBody"></tbody></table></div><label class="checkline mt-3"><input type="checkbox" id="closeConfirm"> Confirmo que la ruta regresa a planta con estas cantidades.</label><textarea id="closeNotes" class="form-control mt-3" rows="2" placeholder="Comentario de cierre"></textarea></div><div class="modal-footer"><button class="btn btn-secondary" type="button" onclick="closeModal('closeDayModal')">Cancelar</button><button class="btn btn-brand" type="button" onclick="closeDay()">Cerrar ruta del día</button></div></div></div></div>

<div class="modal" id="returnModal" tabindex="-1" onclick="modalBackdropClose(event)"><div class="modal-dialog modal-wide"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Devolución a almacén físico ALM01</h5><button class="btn-close" type="button" onclick="closeModal('returnModal')" aria-label="Cerrar"></button></div><div class="modal-body"><div id="returnItems"></div><button class="btn btn-sm btn-outline-secondary" type="button" onclick="addReturnRow()">Agregar artículo</button><textarea id="returnNotes" class="form-control mt-3" rows="2" placeholder="Notas de devolución"></textarea></div><div class="modal-footer"><button class="btn btn-secondary" type="button" onclick="closeModal('returnModal')">Cancelar</button><button class="btn btn-brand" type="button" onclick="saveReturn()">Registrar devolución</button></div></div></div></div>


<button class="smart-float" type="button" onclick="openSmartStatus()" id="smartFloat">
  <span class="smart-icon">🤖</span>
  <span>
    <span class="smart-title" id="smartFloatTitle">Hola, revisando tu ruta...</span>
    <span class="smart-sub" id="smartFloatSub">Cargando existencias y movimientos.</span>
  </span>
  <span class="smart-badge" id="smartFloatBadge">-</span>
</button>

<div class="modal" id="smartStatusModal" tabindex="-1" onclick="modalBackdropClose(event)">
  <div class="modal-dialog modal-wide">
    <div class="modal-content">
      <div class="modal-header">
        <div>
          <h5 class="modal-title" id="smartModalTitle">Estado inteligente de ruta</h5>
          <div class="small-muted" id="smartModalSubtitle">Actualizado automáticamente cada hora.</div>
        </div>
        <button class="btn-close" type="button" onclick="closeModal('smartStatusModal')" aria-label="Cerrar"></button>
      </div>
      <div class="modal-body">
        <div class="smart-grid">
          <div class="smart-card"><div class="label">Piezas en ruta</div><div class="value" id="smartPieces">0</div></div>
          <div class="smart-card"><div class="label">Paquetes aprox.</div><div class="value" id="smartPackages">0</div></div>
          <div class="smart-card"><div class="label">Tarimas aprox.</div><div class="value" id="smartPallets">0</div></div>
        </div>
        <div id="smartWarnings"></div>
        <div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-3 mb-2">
          <div>
            <div class="fw-bold">Existencias y movimiento del día</div>
            <div class="small-muted">Detecta productos cargados que aún no tienen salida hoy.</div>
          </div>
          <button class="btn btn-sm btn-outline-primary" type="button" onclick="refreshSmartStatus(true)">Actualizar ahora</button>
        </div>
        <div id="smartStatusList" class="smart-list"></div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-secondary" type="button" onclick="closeModal('smartStatusModal')">Cerrar</button>
        <button class="btn btn-brand" type="button" onclick="openDelivery();closeModal('smartStatusModal')">Registrar salida</button>
      </div>
    </div>
  </div>
</div>

<script>
const CSRF = <?=json_encode($csrf)?>;
let stock = [];
let allProducts = [];
let currentSummary = [];
let currentUserName = '';
let currentRouteLabel = '';
let currentRouteNo = null;
function routeParam(){ const s=document.getElementById('routeSelect'); return currentRouteNo ? '&route_no='+encodeURIComponent(s && !s.classList.contains('d-none') ? s.value : currentRouteNo) : ''; }
function workDateParam(){ return '&work_date='+encodeURIComponent(document.getElementById('workDate').value || '<?=$defaultWorkDate?>'); }
async function api(action, opts={}){ const res = await fetch(`?action=${action}${routeParam()}${workDateParam()}`, {headers:{'Content-Type':'application/json','X-CSRF':CSRF}, ...opts}); const data = await res.json().catch(()=>({ok:false,error:'Respuesta inválida'})); if(!data.ok) throw new Error(data.error || 'Error'); return data; }
function showAlert(msg,type='success'){ document.getElementById('alertBox').innerHTML = `<div class="alert alert-${type}">${escapeHtml(msg)}<button class="btn-close" type="button" onclick="this.closest('.alert').remove()" aria-label="Cerrar"></button></div>`; }
function escapeHtml(s){return String(s??'').replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));}
function detectDeviceMode(){
  const w = window.innerWidth || document.documentElement.clientWidth;
  const coarse = matchMedia('(hover:none) and (pointer:coarse)').matches;
  let mode = 'PC';
  if (w <= 767) mode = 'Celular';
  else if (w <= 1199 || coarse) mode = 'Tablet';
  document.body.dataset.device = mode.toLowerCase();
  const chip = document.getElementById('deviceChip');
  if (chip) chip.textContent = `Vista ${mode}`;
}
window.addEventListener('resize', detectDeviceMode);
window.addEventListener('orientationchange', detectDeviceMode);

function openModal(id){ const el=document.getElementById(id); if(el){ el.classList.add('show'); document.body.style.overflow='hidden'; } }
function closeModal(id){ const el=document.getElementById(id); if(el){ el.classList.remove('show'); document.body.style.overflow=''; } }
function modalBackdropClose(ev){ if(ev.target && ev.target.classList && ev.target.classList.contains('modal')) closeModal(ev.target.id); }
document.addEventListener('keydown', ev=>{ if(ev.key==='Escape'){ document.querySelectorAll('.modal.show').forEach(m=>closeModal(m.id)); } });

function n(v){ return Number(v||0).toLocaleString('es-MX',{maximumFractionDigits:0}); }
async function bootstrap(){
  const data = await api('bootstrap');
  currentRouteNo = data.route.route_no;
  allProducts = data.reload_products || [];
  currentUserName = (data.user && data.user.name) ? data.user.name : 'operador';
  currentRouteLabel = `${data.route.warehouse_code || ''} · ${data.route.route.route_name || ''}`.trim();
  const kRouteEl=document.getElementById('kRoute'); if(kRouteEl) kRouteEl.textContent = data.route.warehouse_code;
  const routeNameText = `${data.route.route.route_code || ''} ${data.route.route.route_name || ''}`.trim();
  const kRouteNameEl=document.getElementById('kRouteName'); if(kRouteNameEl) kRouteNameEl.textContent = routeNameText;
  const headerRoute=document.getElementById('headerRouteText'); if(headerRoute) headerRoute.textContent = currentRouteLabel || routeNameText || 'Ruta activa';
  const pill=document.getElementById('quickRoutePill'); if(pill) pill.textContent = currentRouteLabel || 'Ruta activa';
  const sel=document.getElementById('routeSelect');
  if(data.routes && data.routes.length){
    const visibleRoutes = data.routes.filter(r => Number(r.route_no)!==1 && !String((r.route_code||'')+' '+(r.route_name||'')).toUpperCase().includes('VENTA'));
    if(visibleRoutes.length){
      sel.classList.remove('d-none');
      sel.innerHTML=visibleRoutes.map(r=>`<option value="${r.route_no}" ${Number(r.route_no)===Number(currentRouteNo)?'selected':''}>Ruta ${r.route_no} · ${escapeHtml(r.route_code||'')} ${escapeHtml(r.route_name||'')}</option>`).join('');
      sel.onchange=()=>{currentRouteNo=sel.value; loadAll();};
    } else {
      sel.classList.add('d-none');
    }
  }
}
async function loadAll(){
  try{
    await bootstrap();
    const data=await api('inventory');
    stock=data.stock||[];
    currentSummary=data.summary||[];
    renderStock();
    renderSummary(currentSummary);
    updateSmartButton();
    loadCustomers();
  }catch(e){ showAlert(e.message,'danger'); }
}
function renderStock(){ let total=0; stock.forEach(x=>total+=Number(x.qty||0)); const kStockEl=document.getElementById('kStock'); if(kStockEl) kStockEl.textContent=n(total); const kProductsEl=document.getElementById('kProducts'); if(kProductsEl) kProductsEl.textContent=n(stock.length); const body=document.getElementById('stockBody'); const closeBody=document.getElementById('closeSummaryBody'); if(!stock.length){ body.innerHTML='<tr><td colspan="4" class="text-center py-4 text-muted">La ruta no tiene inventario actualmente.</td></tr>'; closeBody.innerHTML='<tr><td colspan="4" class="text-center py-4 text-muted">La ruta no tiene inventario para cerrar.</td></tr>'; return;} body.innerHTML=stock.map(x=>`<tr class="product-row" onclick="openDelivery(${x.product_id})"><td><div class="fw-bold">${escapeHtml(x.sku)}</div><div class="small-muted">${escapeHtml(x.name)} · ${escapeHtml(x.packaging_name||'')}</div></td><td class="text-end qty">${n(x.qty)}</td><td class="text-end">${n(x.packages)}</td><td class="text-end">${n(x.pallets)}</td></tr>`).join(''); closeBody.innerHTML=stock.map(x=>`<tr><td><div class="fw-bold">${escapeHtml(x.sku)}</div><div class="small-muted">${escapeHtml(x.name)}</div></td><td class="text-end qty">${n(x.qty)}</td><td class="text-end">${n(x.packages)}</td><td class="text-end">${n(x.pallets)}</td></tr>`).join(''); }
function calcPackages(qty, ppp){ ppp=Number(ppp||0); return ppp>0 ? Math.floor(Number(qty||0)/ppp) : 0; }
function calcPallets(qty, ppp, ppal){ ppp=Number(ppp||0); ppal=Number(ppal||0); return (ppp>0 && ppal>0) ? Math.floor(Number(qty||0)/(ppp*ppal)) : 0; }
function renderSummary(rows){
  let tin=0,tout=0,totalPieces=0,totalPackages=0,totalPallets=0;
  rows.forEach(x=>{tin+=Number(x.in_qty||0);tout+=Number(x.out_qty||0);totalPieces+=Number(x.current_qty||0);totalPackages+=Number(x.current_packages ?? calcPackages(x.current_qty,x.pieces_per_package));totalPallets+=Number(x.current_pallets ?? calcPallets(x.current_qty,x.pieces_per_package,x.packages_per_pallet));});
  const kInEl=document.getElementById('kIn'); if(kInEl) kInEl.textContent=n(tin); const kOutEl=document.getElementById('kOut'); if(kOutEl) kOutEl.textContent=n(tout);
  const tP=document.getElementById('sumTotalPieces'); if(tP) tP.textContent=n(totalPieces);
  const tPk=document.getElementById('sumTotalPackages'); if(tPk) tPk.textContent=n(totalPackages);
  const tTa=document.getElementById('sumTotalPallets'); if(tTa) tTa.textContent=n(totalPallets);
  document.getElementById('summaryBody').innerHTML = rows.length ? rows.map(x=>{ const pk = x.current_packages ?? calcPackages(x.current_qty,x.pieces_per_package); const ta = x.current_pallets ?? calcPallets(x.current_qty,x.pieces_per_package,x.packages_per_pallet); return `<tr><td><b>${escapeHtml(x.sku)}</b><div class="small-muted">${escapeHtml(x.name)}</div></td><td class="text-end">${n(x.initial_qty)}</td><td class="text-end text-success fw-bold">${n(x.in_qty)}</td><td class="text-end text-danger fw-bold">${n(x.out_qty)}</td><td class="text-end qty">${n(x.current_qty)}</td><td class="text-end">${n(pk)}</td><td class="text-end">${n(ta)}</td></tr>`; }).join('') : '<tr><td colspan="7" class="text-center py-4 text-muted">Sin movimientos en la ruta.</td></tr>';
}
function selectOut(pid){ openDelivery(pid); }
async function loadCustomers(){ try{ const res=await fetch(`?action=customers${routeParam()}${workDateParam()}`); const data=await res.json(); if(data.ok){ document.getElementById('customersList').innerHTML=(data.rows||[]).map(c=>`<option value="${escapeHtml(c.business_name||c.contact_name||'')}">`).join(''); }}catch(e){} }
let pendingDeliveryPayload = null;
function productLabelById(productId){ const p = stock.find(x=>String(x.product_id)===String(productId)); return p ? `${p.sku} · ${p.name}` : `Producto #${productId}`; }
function unitLabel(unit){ if(unit==='tarima') return 'Tarima'; if(unit==='paquete') return 'Paquete'; return 'Piezas'; }
function saveRouteOut(){
  try{
    const items=collectItems('deliveryItems');
    const customer=(document.getElementById('outCustomer').value||'').trim();
    if(!customer) throw new Error('Captura el cliente/destino antes de confirmar.');
    if(!items.length) throw new Error('Agrega al menos un producto a entregar.');
    let total=0;
    const body=document.getElementById('confirmDeliveryBody');
    body.innerHTML=items.map(x=>{ total += Number(x.qty||0); return `<tr><td><b>${escapeHtml(productLabelById(x.product_id))}</b></td><td class="text-end">${escapeHtml(unitLabel(x.unit_type))}</td><td class="text-end">${Number(x.amount||0).toLocaleString('es-MX',{maximumFractionDigits:2})}</td><td class="text-end qty">${n(x.qty)}</td></tr>`; }).join('');
    document.getElementById('confirmCustomer').textContent=customer;
    document.getElementById('confirmTotalPieces').textContent=`${n(total)} piezas`;
    pendingDeliveryPayload={items, customer_name:customer, notes:document.getElementById('outNotes').value};
    openModal('confirmDeliveryModal');
  }catch(e){showAlert(e.message,'danger');}
}
async function confirmAndSaveRouteOut(){
  try{
    if(!pendingDeliveryPayload) throw new Error('No hay salida pendiente por confirmar.');
    const data=await api('route_out_save',{method:'POST',body:JSON.stringify(pendingDeliveryPayload)});
    closeModal('confirmDeliveryModal');
    closeModal('deliveryModal');
    showAlert(data.message + ' · Ticket generado.');
    document.getElementById('outCustomer').value='';
    document.getElementById('outNotes').value='';
    document.getElementById('deliveryItems').innerHTML='';
    pendingDeliveryPayload=null;
    await loadAll();
    if(data.ticket_url){ window.open(data.ticket_url, '_blank'); }
  }catch(e){showAlert(e.message,'danger');}
}
function productSelectHtml(source='stock'){ const rows = source==='all' ? (allProducts.length?allProducts:stock) : stock; const extra = source==='all' ? ' data-source="all"' : ''; return '<select class="form-select product-id"'+extra+' onchange="updateRowCalculation(this.closest(\'.item-row\'))"><option value="">Selecciona artículo...</option>'+rows.map(x=>{ const availableTxt = source==='all' ? (x.packaging_name ? ` · ${escapeHtml(x.packaging_name)}` : '') : ` · disponible ${n(x.qty)} pz`; return `<option value="${x.product_id}" data-pkg="${x.packaging_option_id||0}" data-ppp="${x.pieces_per_package||0}" data-ppal="${x.packages_per_pallet||0}" data-available="${x.qty||0}">${escapeHtml(x.sku)} · ${escapeHtml(x.name)}${availableTxt}</option>`; }).join('')+'</select>'; }
function unitSelectHtml(){ return `<select class="form-select unit-type" onchange="updateRowCalculation(this.closest('.item-row'))"><option value="tarima">📦 Tarima</option><option value="paquete">🧃 Paquete</option><option value="pieza" selected>🔢 Piezas</option></select>`; }
function renderDeliveryEmpty(){ const box=document.getElementById('deliveryItems'); if(!box) return; const rows=box.querySelectorAll('.item-row'); const empty=box.querySelector('.empty-delivery'); if(rows.length===0 && !empty){ box.innerHTML='<div class="empty-delivery">📦 Agrega al menos un producto entregado en esta visita.</div>'; } else if(rows.length>0 && empty){ empty.remove(); } }
function adjustAmount(btn,delta){ const row=btn.closest('.item-row'); const input=row?.querySelector('.amount'); if(!input) return; const unit=row.querySelector('.unit-type')?.value || 'pieza'; const step=unit==='tarima'?0.25:1; const current=Number(input.value||0); const next=Math.max(0, current + (delta*step)); input.value = unit==='tarima' ? Number(next.toFixed(2)) : Math.round(next); updateRowCalculation(row); }
function removeDeliveryRow(btn){ const row=btn.closest('.item-row'); if(row) row.remove(); renderDeliveryEmpty(); }
function addDeliveryRow(productId=''){ const html = `<div class="item-row delivery-row delivery-card-row"><div class="delivery-row-head"><div class="delivery-row-title"><div class="icon">📦</div><div><strong>Producto entregado</strong><small>Selecciona artículo, unidad y cantidad</small></div></div><button class="btn btn-remove-icon" type="button" title="Quitar producto" onclick="removeDeliveryRow(this)">🗑️</button></div><div class="delivery-row-grid"><div class="field-box field-product"><label>🔎 Artículo</label>${productSelectHtml()}</div><div class="field-box"><label>📐 Unidad</label>${unitSelectHtml()}</div><div class="field-box"><label>➕ Cantidad</label><div class="qty-stepper"><button class="step-btn minus" type="button" onclick="adjustAmount(this,-1)">−</button><input class="form-control amount text-center" type="number" min="0" step="0.01" placeholder="0" oninput="updateRowCalculation(this.closest('.item-row'))"><button class="step-btn" type="button" onclick="adjustAmount(this,1)">＋</button></div><small class="qty-hint">Usa + / − para ajustar rápido.</small></div><div class="calc-preview"><span class="calc-icon">🧮</span><div><div class="calc-main">Equivale a 0 piezas</div><div class="calc-sub">Selecciona producto</div></div></div></div></div>`; const box=document.getElementById('deliveryItems'); const empty=box.querySelector('.empty-delivery'); if(empty) empty.remove(); box.insertAdjacentHTML('beforeend', html); const rows=document.querySelectorAll('#deliveryItems .item-row'); const row=rows[rows.length-1]; if(productId){ row.querySelector('.product-id').value=productId; row.querySelector('.amount').focus(); updateRowCalculation(row); } else { row.querySelector('.product-id')?.focus(); } renderDeliveryEmpty(); }
function rowPieces(row){ const opt=row.querySelector('.product-id')?.selectedOptions?.[0]; const unit=row.querySelector('.unit-type')?.value || 'pieza'; const amount=Number(row.querySelector('.amount')?.value || row.querySelector('.qty')?.value || 0); const ppp=Number(opt?.dataset.ppp || 0); const ppal=Number(opt?.dataset.ppal || 0); let pieces=0; if(unit==='tarima') pieces=amount*ppal*ppp; else if(unit==='paquete') pieces=amount*ppp; else pieces=amount; return Math.round(pieces); }
function updateRowCalculation(row){ if(!row) return; const preview=row.querySelector('.calc-preview'); const main=preview?.querySelector('.calc-main'); const sub=preview?.querySelector('.calc-sub'); const opt=row.querySelector('.product-id')?.selectedOptions?.[0]; const pieces=rowPieces(row); const available=Number(opt?.dataset.available || 0); const mode=row.dataset.mode||''; preview?.classList.remove('warn','danger','ok'); if(!opt || !opt.value){ if(main) main.textContent='Equivale a 0 piezas'; if(sub) sub.textContent='Selecciona producto'; return; } if(main) main.textContent=`Equivale a ${n(pieces)} piezas`; if(mode==='request'){ preview?.classList.add(pieces>0?'ok':'warn'); const ppp=Number(opt?.dataset.ppp||0), ppal=Number(opt?.dataset.ppal||0); if(sub) sub.textContent=pieces>0?`Solicitud de recarga para almacén · ${ppp||0} pz/paq · ${ppal||0} paq/tar`:'Captura la cantidad que pide el cliente'; return; } if(pieces<=0){ if(sub) sub.textContent=`Disponible en ruta: ${n(available)} piezas`; return; } if(pieces>available){ preview?.classList.add('danger'); if(sub) sub.textContent=`No alcanza: disponible ${n(available)} piezas`; } else if(available>0 && pieces>=available*0.85){ preview?.classList.add('warn'); if(sub) sub.textContent=`Inventario justo: quedarán ${n(available-pieces)} piezas`; } else { preview?.classList.add('ok'); if(sub) sub.textContent=`Disponible: ${n(available)} · quedarán ${n(available-pieces)} piezas`; } }
function openInventory(){ openModal('inventoryModal'); }
function openSummary(){ openModal('summaryModal'); }
function openKpiModal(){ openModal('kpiModal'); }
function openDelivery(productId=''){ const ref=document.getElementById('outRefPreview'); if(ref) ref.textContent='Se generará automáticamente al registrar'; const scanBox=document.getElementById('customerScanResult'); if(scanBox){ scanBox.classList.add('d-none'); scanBox.textContent=''; } document.getElementById('deliveryItems').innerHTML=''; addDeliveryRow(productId); openModal('deliveryModal'); }
function openCloseDay(){ document.getElementById('closeConfirm').checked=false; openModal('closeDayModal'); }
function addReloadRow(){ const html=`<div class="item-row delivery-row delivery-card-row reload-card-row" data-mode="request"><div class="delivery-row-head"><div class="delivery-row-title"><div class="icon">🔄</div><div><strong>Artículo solicitado</strong><small>Puede ser cualquier producto activo de la planta</small></div></div><button class="btn btn-remove-icon" type="button" title="Quitar producto" onclick="this.closest('.item-row').remove()">🗑️</button></div><div class="delivery-row-grid"><div class="field-box field-product"><label>🔎 Artículo de planta</label>${productSelectHtml('all')}</div><div class="field-box"><label>📐 Unidad</label>${unitSelectHtml()}</div><div class="field-box"><label>➕ Cantidad a pedir</label><div class="qty-stepper"><button class="step-btn minus" type="button" onclick="adjustAmount(this,-1)">−</button><input class="form-control amount text-center" type="number" min="0" step="0.01" placeholder="0" oninput="updateRowCalculation(this.closest('.item-row'))"><button class="step-btn" type="button" onclick="adjustAmount(this,1)">＋</button></div><small class="qty-hint">Solicita tarimas, paquetes o piezas.</small></div><div class="calc-preview"><span class="calc-icon">🧮</span><div><div class="calc-main">Equivale a 0 piezas</div><div class="calc-sub">Selecciona producto</div></div></div></div></div>`; document.getElementById('reloadItems').insertAdjacentHTML('beforeend', html); }
function addReturnRow(){ document.getElementById('returnItems').insertAdjacentHTML('beforeend', `<div class="row g-2 align-items-end mb-2 item-row"><div class="col-md-7">${productSelectHtml()}</div><div class="col-md-3"><input class="form-control qty" type="number" min="1" step="1" placeholder="Piezas"></div><div class="col-md-2"><button class="btn btn-outline-danger w-100" onclick="this.closest('.item-row').remove()">Quitar</button></div></div>`); }
function openReload(){ document.getElementById('reloadItems').innerHTML=''; addReloadRow(); openModal('reloadModal'); }
function openReturn(){ document.getElementById('returnItems').innerHTML=''; addReturnRow(); openModal('returnModal'); }
function collectItems(containerId){
  return [...document.querySelectorAll(`#${containerId} .item-row`)].map(r=>{
    const product=r.querySelector('.product-id');
    const amountInput=r.querySelector('.amount') || r.querySelector('.qty');
    const unit=r.querySelector('.unit-type');
    const amount=Number(amountInput?.value || 0);
    return {product_id:product?.value || '', unit_type:unit?.value || 'pieza', amount:amount, qty:rowPieces(r), packaging_option_id:product?.selectedOptions?.[0]?.dataset.pkg||0};
  }).filter(x=>x.product_id && Number(x.amount)>0);
}
async function saveReload(){ try{ const data=await api('reload_request_save',{method:'POST',body:JSON.stringify({items:collectItems('reloadItems'),notes:document.getElementById('reloadNotes').value})}); closeModal('reloadModal'); showAlert(data.message); }catch(e){showAlert(e.message,'danger');} }
async function saveReturn(){ try{ const data=await api('return_to_warehouse_save',{method:'POST',body:JSON.stringify({items:collectItems('returnItems'),notes:document.getElementById('returnNotes').value})}); closeModal('returnModal'); showAlert(data.message); await loadAll(); }catch(e){showAlert(e.message,'danger');} }
async function closeDay(){ try{ if(!document.getElementById('closeConfirm').checked) throw new Error('Confirma que la ruta regresa a planta con las cantidades mostradas.'); const data=await api('close_day',{method:'POST',body:JSON.stringify({notes:document.getElementById('closeNotes').value})}); closeModal('closeDayModal'); showAlert(data.message); }catch(e){showAlert(e.message,'danger');} }

function smartTotals(){
  let pieces=0, packages=0, pallets=0;
  stock.forEach(x=>{
    pieces += Number(x.qty||0);
    packages += Number(x.packages ?? calcPackages(x.qty,x.pieces_per_package));
    pallets += Number(x.pallets ?? calcPallets(x.qty,x.pieces_per_package,x.packages_per_pallet));
  });
  return {pieces, packages, pallets};
}
function smartNoMovementRows(){
  const summaryByProduct = new Map((currentSummary||[]).map(x=>[String(x.product_id), x]));
  return (stock||[]).filter(x=>{
    const s = summaryByProduct.get(String(x.product_id));
    return Number(x.qty||0) > 0 && (!s || Number(s.out_qty||0) <= 0);
  });
}
function updateSmartButton(){
  const totals=smartTotals();
  const noMove=smartNoMovementRows();
  const firstName=String(currentUserName||'operador').split(/\s+/)[0] || 'operador';
  const title=document.getElementById('smartFloatTitle');
  const sub=document.getElementById('smartFloatSub');
  const badge=document.getElementById('smartFloatBadge');
  if(title) title.textContent = `Hola ${firstName}, llevas ${n(totals.pieces)} piezas`;
  if(sub) sub.textContent = `${currentRouteLabel || 'Ruta activa'} · ${noMove.length ? noMove.length+' artículo(s) sin salida hoy' : 'todos los artículos cargados tienen movimiento'}`;
  if(badge) badge.textContent = noMove.length ? 'Revisar' : 'OK';
}
function renderSmartStatus(){
  const totals=smartTotals();
  const noMove=smartNoMovementRows();
  const summaryByProduct = new Map((currentSummary||[]).map(x=>[String(x.product_id), x]));
  const firstName=String(currentUserName||'operador').split(/\s+/)[0] || 'operador';
  const t=document.getElementById('smartModalTitle'); if(t) t.textContent = `Hola ${firstName}, estado de ${currentRouteLabel || 'tu ruta'}`;
  const sub=document.getElementById('smartModalSubtitle'); if(sub) sub.textContent = `Actualizado ${new Date().toLocaleString('es-MX')} · revisión automática cada hora.`;
  document.getElementById('smartPieces').textContent=n(totals.pieces);
  document.getElementById('smartPackages').textContent=n(totals.packages);
  document.getElementById('smartPallets').textContent=n(totals.pallets);
  const warnings=document.getElementById('smartWarnings');
  if(warnings){
    warnings.innerHTML = noMove.length
      ? `<div class="smart-notice">No se han detectado salidas hoy para ${noMove.length} artículo(s) que todavía van cargados en la ruta. Revísalos antes de terminar el recorrido.</div>`
      : `<div class="smart-notice smart-ok">Todos los artículos cargados con existencia tienen movimiento de salida detectado el día de hoy.</div>`;
  }
  const list=document.getElementById('smartStatusList');
  if(list){
    list.innerHTML = (stock||[]).length ? stock.map(x=>{
      const s=summaryByProduct.get(String(x.product_id));
      const outQty=Number(s?.out_qty||0);
      const status = outQty>0 ? `<span class="smart-chip">Salida ${n(outQty)}</span>` : `<span class="smart-chip warn">Sin salida hoy</span>`;
      return `<div class="smart-item">
        <div class="name"><div class="sku">${escapeHtml(x.sku)}</div><div class="desc">${escapeHtml(x.name)} · ${escapeHtml(x.packaging_name||'')}</div></div>
        <span class="smart-chip">${n(x.qty)} pz</span>
        <span class="smart-chip">${n(x.packages)} paq</span>
        ${status}
      </div>`;
    }).join('') : '<div class="text-center text-muted py-4">La ruta no tiene inventario cargado.</div>';
  }
}
async function refreshSmartStatus(openAfter=false){
  try{
    const data=await api('inventory');
    stock=data.stock||[];
    currentSummary=data.summary||[];
    renderStock();
    renderSummary(currentSummary);
    updateSmartButton();
    renderSmartStatus();
    if(openAfter) showAlert('Estado inteligente actualizado.');
  }catch(e){ showAlert(e.message,'danger'); }
}
function openSmartStatus(){
  renderSmartStatus();
  openModal('smartStatusModal');
}
setInterval(()=>refreshSmartStatus(false), 60*60*1000);


/* ============================================================
   ESCÁNER DE CLIENTE CON CÁMARA
   - Mantiene captura manual y selección por lista.
   - Si el navegador no soporta BarcodeDetector, abre cámara y deja captura manual.
   - Requiere HTTPS o localhost para que getUserMedia funcione.
============================================================ */
let customerCameraStream = null;
let customerScanTimer = null;
let customerBarcodeDetector = null;
let customerScannerBusy = false;

function setCustomerScanStatus(message, type=''){
  const el = document.getElementById('customerScanStatus');
  if(!el) return;
  el.classList.remove('ok','warn');
  if(type) el.classList.add(type);
  el.textContent = message;
}

function normalizeCustomerScanCode(raw){
  let code = String(raw || '').trim();
  if(!code) return '';
  // Permite QR con JSON simple: {"customer_code":"123"} o {"cliente":"ABC"}
  try{
    const obj = JSON.parse(code);
    if(obj && typeof obj === 'object'){
      code = String(obj.customer_code || obj.codigo_cliente || obj.cliente_codigo || obj.cliente || obj.customer || obj.name || obj.nombre || code).trim();
    }
  }catch(e){}
  // Permite textos como CLIENTE:12345, CODIGO=12345, CUSTOMER_CODE|12345
  const m = code.match(/(?:CLIENTE|CUSTOMER|CUSTOMER_CODE|CODIGO|C[OÓ]DIGO)\s*[:=|\-]\s*([^;|\n\r]+)/i);
  if(m && m[1]) code = m[1].trim();
  return code;
}

async function resolveCustomerCode(code){
  code = normalizeCustomerScanCode(code);
  if(!code){
    setCustomerScanStatus('No se recibió código para buscar.', 'warn');
    return;
  }
  setCustomerScanStatus('Buscando cliente: ' + code + ' ...');
  try{
    const res = await fetch(`?action=customers${routeParam()}${workDateParam()}&q=${encodeURIComponent(code)}`, {headers:{'X-CSRF':CSRF}});
    const data = await res.json().catch(()=>({ok:false, rows:[]}));
    let customerName = code;
    let msg = 'Código capturado. Se usará como cliente/destino.';
    if(data.ok && Array.isArray(data.rows) && data.rows.length){
      const exact = data.rows.find(c => String(c.customer_code||'').toUpperCase() === code.toUpperCase()) || data.rows[0];
      customerName = String(exact.business_name || exact.contact_name || exact.customer_code || code).trim();
      msg = 'Cliente encontrado: ' + customerName;
    }
    const input = document.getElementById('outCustomer');
    if(input) input.value = customerName;
    const result = document.getElementById('customerScanResult');
    if(result){
      result.classList.remove('d-none');
      result.textContent = msg;
    }
    setCustomerScanStatus(msg, 'ok');
    setTimeout(()=>closeCustomerCameraScanner(), 650);
  }catch(e){
    setCustomerScanStatus('No se pudo buscar el cliente. Captura manualmente o intenta otra vez.', 'warn');
  }
}

function resolveCustomerCodeFromManual(){
  const input = document.getElementById('manualCustomerCode');
  resolveCustomerCode(input ? input.value : '');
}

async function openCustomerCameraScanner(){
  openModal('customerCameraModal');
  setCustomerScanStatus('Solicitando permiso de cámara...');
  const manual = document.getElementById('manualCustomerCode');
  if(manual) manual.value = '';

  if(!window.isSecureContext){
    setCustomerScanStatus('El navegador bloqueó la cámara porque la página no está en HTTPS. Usa captura manual.', 'warn');
    return;
  }
  if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){
    setCustomerScanStatus('Este navegador no permite abrir cámara desde la página. Usa captura manual.', 'warn');
    return;
  }

  try{
    closeCustomerCameraStreamOnly();
    const constraints = {
      video: {
        facingMode: { ideal: 'environment' },
        width: { ideal: 1280 },
        height: { ideal: 720 }
      },
      audio: false
    };
    customerCameraStream = await navigator.mediaDevices.getUserMedia(constraints);
    const video = document.getElementById('customerScanVideo');
    if(video){
      video.srcObject = customerCameraStream;
      video.setAttribute('playsinline','true');
      video.muted = true;
      await video.play();
    }

    if('BarcodeDetector' in window){
      try{
        customerBarcodeDetector = new BarcodeDetector({formats:['qr_code','code_128','code_39','ean_13','ean_8','upc_a','upc_e','itf']});
        setCustomerScanStatus('Cámara activa. Apunta al código del cliente.');
        startCustomerDetectLoop();
      }catch(e){
        setCustomerScanStatus('Cámara activa, pero el lector automático no está disponible. Captura el código manualmente.', 'warn');
      }
    }else{
      setCustomerScanStatus('Cámara activa, pero este navegador no trae lector automático. Captura el código manualmente.', 'warn');
    }
  }catch(e){
    let msg = 'No se pudo abrir la cámara.';
    if(e && (e.name === 'NotAllowedError' || e.name === 'PermissionDeniedError')) msg = 'Permiso de cámara denegado. Autoriza la cámara en el navegador o usa captura manual.';
    else if(e && (e.name === 'NotFoundError' || e.name === 'DevicesNotFoundError')) msg = 'No se encontró cámara disponible en el dispositivo.';
    else if(e && e.message) msg += ' ' + e.message;
    setCustomerScanStatus(msg, 'warn');
  }
}

function startCustomerDetectLoop(){
  stopCustomerDetectLoop();
  customerScanTimer = setInterval(async ()=>{
    if(customerScannerBusy || !customerBarcodeDetector) return;
    const modal = document.getElementById('customerCameraModal');
    if(!modal || !modal.classList.contains('show')) return;
    const video = document.getElementById('customerScanVideo');
    if(!video || video.readyState < 2) return;
    customerScannerBusy = true;
    try{
      const codes = await customerBarcodeDetector.detect(video);
      if(codes && codes.length){
        const raw = String(codes[0].rawValue || '').trim();
        if(raw){
          stopCustomerDetectLoop();
          await resolveCustomerCode(raw);
        }
      }
    }catch(e){
      // Algunos navegadores fallan intermitentemente en detect(); no cerramos la cámara.
    }finally{
      customerScannerBusy = false;
    }
  }, 450);
}

function stopCustomerDetectLoop(){
  if(customerScanTimer){
    clearInterval(customerScanTimer);
    customerScanTimer = null;
  }
}

function closeCustomerCameraStreamOnly(){
  stopCustomerDetectLoop();
  if(customerCameraStream){
    customerCameraStream.getTracks().forEach(t=>{ try{ t.stop(); }catch(e){} });
    customerCameraStream = null;
  }
  const video = document.getElementById('customerScanVideo');
  if(video) video.srcObject = null;
}

function closeCustomerCameraScanner(){
  closeCustomerCameraStreamOnly();
  closeModal('customerCameraModal');
  // Si el modal de salida sigue abierto, mantenemos bloqueado el scroll del fondo.
  if(document.getElementById('deliveryModal')?.classList.contains('show')) document.body.style.overflow='hidden';
}

detectDeviceMode();
loadAll();
</script>
</body>
</html>
