<?php
/**
 * produccion_botella.php
 * ============================================================
 * BReport · PRODUCCIÓN BOTELLA · v1
 * ------------------------------------------------------------
 * Primera versión operativa para Planta Botella.
 *
 * Reglas principales:
 * - Módulo exclusivo para scope Botella.
 * - 3 líneas de producción configurables en production_lines.
 * - Una misma línea NO puede tener dos sesiones RUNNING.
 * - El operador puede iniciar turno por línea y capturar producción manual.
 * - Al cierre de turno puede registrar la ÚLTIMA TARIMA con paquetes/piezas sueltas.
 * - PLC usa la tabla general production_plc_tags filtrada por company_id/location_id.
 * - Toda producción física genera IN real en inventory_moves.
 * - Se genera etiqueta por captura en production_labels.
 * - No se crean tablas PLC por planta.
 * ============================================================
 */

declare(strict_types=1);

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

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

require_scope();
ensure_session();

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

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

if (!function_exists('h')) {
    function h($v): string { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }
}

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

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

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

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

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

function botella_add_col(PDO $pdo, string $table, string $col, string $ddl): void {
    try {
        if (botella_table_exists($pdo, $table) && !botella_col_exists($pdo, $table, $col)) {
            $pdo->exec("ALTER TABLE `{$table}` ADD COLUMN {$ddl}");
        }
    } catch (Throwable $e) {}
}

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

function botella_insert_row(PDO $pdo, string $table, array $data): int {
    $cols = [];
    foreach ($data as $k => $_) {
        if (botella_col_exists($pdo, $table, $k)) $cols[] = $k;
    }
    if (!$cols) throw new RuntimeException('No hay columnas válidas para insertar en ' . $table);
    $sql = "INSERT INTO `{$table}` (`" . implode('`,`', $cols) . "`) VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")";
    $params = [];
    foreach ($cols as $c) $params[] = $data[$c];
    $st = $pdo->prepare($sql);
    $st->execute($params);
    return (int)$pdo->lastInsertId();
}

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

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

function botella_shift_now(): int {
    $now = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TZ));
    $hm = $now->format('H:i');
    if ($hm >= '07:00' && $hm < '15:00') return 1;
    if ($hm >= '15:00' && $hm < '22:30') return 2;
    return 3;
}

function botella_is_admin(): bool {
    return has_role('admin') || has_role('corp') || has_role('plant_admin') || has_role('supervisor');
}

function botella_can_produce(): bool {
    return botella_is_admin() || has_role('production') || has_role('produccion') || has_role('warehouse') || has_role('almacen');
}

function botella_scope_names(PDO $pdo, int $companyId, int $locationId): array {
    $company = 'Empresa #' . $companyId;
    $location = 'Localidad #' . $locationId;
    try {
        $st = $pdo->prepare("SELECT name FROM companies WHERE id=? LIMIT 1");
        $st->execute([$companyId]);
        $company = (string)($st->fetchColumn() ?: $company);
        $st = $pdo->prepare("SELECT name FROM locations WHERE id=? AND company_id=? LIMIT 1");
        $st->execute([$locationId, $companyId]);
        $location = (string)($st->fetchColumn() ?: $location);
    } catch (Throwable $e) {}
    return [$company, $location];
}

function botella_assert_scope(PDO $pdo, int $companyId, int $locationId): void {
    [$c, $l] = botella_scope_names($pdo, $companyId, $locationId);
    $txt = mb_strtoupper($c . ' ' . $l, 'UTF-8');
    if (!str_contains($txt, 'BOTELLA')) {
        http_response_code(403);
        exit('Este módulo corresponde únicamente a Planta Botella. Selecciona Botella desde Dashboard.');
    }
}

function botella_ensure_schema(PDO $pdo, int $companyId, int $locationId, int $userId): void {
    if (!botella_table_exists($pdo, 'production_lines')) {
        $pdo->exec("CREATE TABLE production_lines (
            id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT(20) UNSIGNED NOT NULL,
            location_id BIGINT(20) UNSIGNED NOT NULL,
            line_no TINYINT UNSIGNED NOT NULL,
            line_code VARCHAR(40) DEFAULT NULL,
            line_name VARCHAR(120) NOT NULL,
            active TINYINT(1) NOT NULL DEFAULT 1,
            sort_order INT NOT NULL DEFAULT 0,
            created_by BIGINT(20) UNSIGNED DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY uq_production_lines_scope_no (company_id, location_id, line_no),
            KEY idx_production_lines_scope_active (company_id, location_id, active)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }
    if (!botella_table_exists($pdo, 'production_day_targets')) {
        $pdo->exec("CREATE TABLE production_day_targets (
            id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT(20) UNSIGNED NOT NULL,
            location_id BIGINT(20) UNSIGNED NOT NULL,
            work_date DATE NOT NULL,
            product_id BIGINT(20) UNSIGNED DEFAULT NULL,
            target_pieces DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            notes VARCHAR(255) DEFAULT NULL,
            created_by BIGINT(20) UNSIGNED DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_by BIGINT(20) UNSIGNED DEFAULT NULL,
            updated_at DATETIME DEFAULT NULL,
            PRIMARY KEY (id),
            KEY idx_production_day_targets_date (company_id, location_id, work_date)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }
    if (!botella_table_exists($pdo, 'production_signal_settings')) {
        $pdo->exec("CREATE TABLE production_signal_settings (
            id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT(20) UNSIGNED NOT NULL,
            location_id BIGINT(20) UNSIGNED NOT NULL,
            report_mode VARCHAR(20) NOT NULL DEFAULT 'MANUAL',
            plc_enabled TINYINT(1) NOT NULL DEFAULT 0,
            poll_seconds INT NOT NULL DEFAULT 10,
            default_product_id BIGINT(20) UNSIGNED DEFAULT NULL,
            default_warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL,
            default_packaging_option_id BIGINT(20) UNSIGNED DEFAULT NULL,
            updated_by BIGINT(20) UNSIGNED DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY uq_production_signal_settings_scope (company_id, location_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    } else {
        botella_add_col($pdo, 'production_signal_settings', 'report_mode', "report_mode VARCHAR(20) NOT NULL DEFAULT 'MANUAL' AFTER location_id");
        botella_add_col($pdo, 'production_signal_settings', 'plc_enabled', "plc_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER report_mode");
        botella_add_col($pdo, 'production_signal_settings', 'poll_seconds', "poll_seconds INT NOT NULL DEFAULT 10 AFTER plc_enabled");
        botella_add_col($pdo, 'production_signal_settings', 'default_product_id', "default_product_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER poll_seconds");
        botella_add_col($pdo, 'production_signal_settings', 'default_warehouse_id', "default_warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER default_product_id");
        botella_add_col($pdo, 'production_signal_settings', 'default_packaging_option_id', "default_packaging_option_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER default_warehouse_id");
        botella_add_col($pdo, 'production_signal_settings', 'updated_by', "updated_by BIGINT(20) UNSIGNED DEFAULT NULL AFTER default_packaging_option_id");
        botella_add_col($pdo, 'production_signal_settings', 'created_at', "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER updated_by");
        botella_add_col($pdo, 'production_signal_settings', 'updated_at', "updated_at DATETIME DEFAULT NULL AFTER created_at");
        botella_add_index($pdo, 'production_signal_settings', 'uq_production_signal_settings_scope', "UNIQUE KEY uq_production_signal_settings_scope (company_id, location_id)");
    }
    $stSet = $pdo->prepare("SELECT id FROM production_signal_settings WHERE company_id=? AND location_id=? LIMIT 1");
    $stSet->execute([$companyId, $locationId]);
    if (!$stSet->fetchColumn()) {
        botella_insert_row($pdo, 'production_signal_settings', [
            'company_id'=>$companyId,
            'location_id'=>$locationId,
            'report_mode'=>'MANUAL',
            'plc_enabled'=>0,
            'poll_seconds'=>10,
            'created_at'=>botella_now(),
        ]);
    }

    if (!botella_table_exists($pdo, 'production_labels')) {
        $pdo->exec("CREATE TABLE production_labels (
            id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT(20) UNSIGNED NOT NULL,
            location_id BIGINT(20) UNSIGNED NOT NULL,
            work_date DATE NOT NULL,
            shift TINYINT UNSIGNED NOT NULL,
            production_line_id BIGINT(20) UNSIGNED DEFAULT NULL,
            production_line_no TINYINT UNSIGNED DEFAULT NULL,
            product_id BIGINT(20) UNSIGNED NOT NULL,
            packaging_option_id BIGINT(20) UNSIGNED DEFAULT NULL,
            capture_group_id BIGINT(20) UNSIGNED DEFAULT NULL,
            inventory_move_id BIGINT(20) UNSIGNED DEFAULT NULL,
            label_code VARCHAR(80) NOT NULL,
            pallets_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            packages_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            loose_packages DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            loose_pieces DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            total_pieces DECIMAL(14,3) NOT NULL DEFAULT 0.000,
            status VARCHAR(30) NOT NULL DEFAULT 'ACTIVA',
            printed_at DATETIME DEFAULT NULL,
            printed_by BIGINT(20) UNSIGNED DEFAULT NULL,
            created_by BIGINT(20) UNSIGNED DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY uq_production_labels_code (company_id, location_id, label_code),
            KEY idx_production_labels_scope_date (company_id, location_id, work_date, shift)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }

    botella_add_col($pdo, 'shift_sessions', 'production_line_id', "production_line_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER location_id");
    botella_add_col($pdo, 'shift_sessions', 'production_line_no', "production_line_no TINYINT UNSIGNED DEFAULT NULL AFTER production_line_id");
    botella_add_col($pdo, 'shift_sessions', 'line_name', "line_name VARCHAR(120) DEFAULT NULL AFTER production_line_no");
    botella_add_col($pdo, 'shift_sessions', 'end_notes', "end_notes VARCHAR(255) DEFAULT NULL AFTER status");
    botella_add_col($pdo, 'shift_sessions', 'report_mode', "report_mode VARCHAR(20) NOT NULL DEFAULT 'MANUAL' AFTER end_notes");
    botella_add_col($pdo, 'shift_sessions', 'plc_tag_id', "plc_tag_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER report_mode");
    botella_add_index($pdo, 'shift_sessions', 'idx_shift_sessions_line_running', "INDEX idx_shift_sessions_line_running (company_id, location_id, production_line_no, status)");

    botella_add_col($pdo, 'production_plc_tags', 'enabled', "enabled TINYINT(1) NOT NULL DEFAULT 1 AFTER active");
    botella_add_col($pdo, 'production_plc_tags', 'production_line_id', "production_line_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER line_name");
    botella_add_col($pdo, 'production_plc_tags', 'production_line_no', "production_line_no TINYINT UNSIGNED DEFAULT NULL AFTER production_line_id");
    botella_add_col($pdo, 'production_plc_tags', 'packaging_option_id', "packaging_option_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER product_id");
    botella_add_col($pdo, 'production_plc_tags', 'warehouse_id', "warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER packaging_option_id");
    botella_add_col($pdo, 'production_plc_tags', 'pallets_per_signal', "pallets_per_signal DECIMAL(14,3) NOT NULL DEFAULT 1.000 AFTER qty_per_signal");
    botella_add_col($pdo, 'production_plc_tags', 'packages_per_signal', "packages_per_signal DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER pallets_per_signal");
    botella_add_col($pdo, 'production_plc_tags', 'pieces_per_signal', "pieces_per_signal DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER packages_per_signal");
    /*
     * Columnas de control PLC automático.
     * En instalaciones existentes puede no existir alguna de estas columnas.
     * Se agregan para que el cierre automático de señal no falle con:
     * Unknown column 'last_signal_state' in 'SET'.
     */
    botella_add_col($pdo, 'production_plc_tags', 'signal_state', "signal_state TINYINT(1) NOT NULL DEFAULT 0 AFTER pieces_per_signal");
    botella_add_col($pdo, 'production_plc_tags', 'last_signal_state', "last_signal_state TINYINT(1) NOT NULL DEFAULT 0 AFTER signal_state");
    botella_add_col($pdo, 'production_plc_tags', 'status', "status VARCHAR(30) NOT NULL DEFAULT 'IDLE' AFTER last_signal_state");
    botella_add_col($pdo, 'production_plc_tags', 'handled_at', "handled_at DATETIME DEFAULT NULL AFTER status");
    botella_add_col($pdo, 'production_plc_tags', 'handled_by', "handled_by BIGINT(20) UNSIGNED DEFAULT NULL AFTER handled_at");
    botella_add_col($pdo, 'production_plc_tags', 'last_action', "last_action VARCHAR(40) DEFAULT NULL AFTER handled_by");
    botella_add_col($pdo, 'production_plc_tags', 'consumed_at', "consumed_at DATETIME DEFAULT NULL AFTER last_action");
    botella_add_col($pdo, 'production_plc_tags', 'notes', "notes VARCHAR(255) DEFAULT NULL AFTER consumed_at");
    botella_add_index($pdo, 'production_plc_tags', 'idx_prod_plc_tags_line', "INDEX idx_prod_plc_tags_line (company_id, location_id, production_line_no, active)");

    foreach (['production_capture_groups','production_entries'] as $t) {
        botella_add_col($pdo, $t, 'production_line_id', "production_line_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER location_id");
        botella_add_col($pdo, $t, 'production_line_no', "production_line_no TINYINT UNSIGNED DEFAULT NULL AFTER production_line_id");
        botella_add_col($pdo, $t, 'packaging_option_id', "packaging_option_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER product_id");
        if ($t === 'production_capture_groups') {
            botella_add_col($pdo, $t, 'warehouse_id', "warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER packaging_option_id");
            botella_add_col($pdo, $t, 'pallets_qty', "pallets_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER qty_total");
            botella_add_col($pdo, $t, 'packages_qty', "packages_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER pallets_qty");
            botella_add_col($pdo, $t, 'loose_packages', "loose_packages DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER packages_qty");
            botella_add_col($pdo, $t, 'loose_pieces', "loose_pieces DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER loose_packages");
            botella_add_col($pdo, $t, 'source_type', "source_type VARCHAR(30) DEFAULT 'MANUAL' AFTER loose_pieces");
            botella_add_col($pdo, $t, 'plc_tag_id', "plc_tag_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER source_type");
            botella_add_col($pdo, $t, 'label_id', "label_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER plc_tag_id");
        } else {
            botella_add_col($pdo, $t, 'pallets_qty', "pallets_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER qty");
            botella_add_col($pdo, $t, 'packages_qty', "packages_qty DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER pallets_qty");
            botella_add_col($pdo, $t, 'loose_packages', "loose_packages DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER packages_qty");
            botella_add_col($pdo, $t, 'loose_pieces', "loose_pieces DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER loose_packages");
            botella_add_col($pdo, $t, 'label_id', "label_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER capture_uid");
        }
    }
    botella_add_index($pdo, 'production_capture_groups', 'idx_prod_capture_line', "INDEX idx_prod_capture_line (company_id, location_id, work_date, shift, production_line_no)");
    botella_add_index($pdo, 'production_entries', 'idx_production_entries_line', "INDEX idx_production_entries_line (company_id, location_id, work_date, shift, production_line_no)");

    if (!botella_table_exists($pdo, 'production_plc_signal_events')) {
        $pdo->exec("CREATE TABLE production_plc_signal_events (
            id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT(20) UNSIGNED NOT NULL,
            location_id BIGINT(20) UNSIGNED NOT NULL,
            tag_id BIGINT(20) UNSIGNED DEFAULT NULL,
            tag_code VARCHAR(80) DEFAULT NULL,
            production_line_id BIGINT(20) UNSIGNED DEFAULT NULL,
            production_line_no TINYINT UNSIGNED DEFAULT NULL,
            work_date DATE DEFAULT NULL,
            shift TINYINT UNSIGNED DEFAULT NULL,
            signal_value TINYINT(1) NOT NULL DEFAULT 1,
            event_type VARCHAR(40) DEFAULT NULL,
            event_status VARCHAR(30) NOT NULL DEFAULT 'PENDING',
            detected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            processed_at DATETIME DEFAULT NULL,
            processed_by BIGINT(20) UNSIGNED DEFAULT NULL,
            production_capture_uid VARCHAR(64) DEFAULT NULL,
            downtime_id BIGINT(20) UNSIGNED DEFAULT NULL,
            notes VARCHAR(255) DEFAULT NULL,
            created_by BIGINT(20) UNSIGNED DEFAULT NULL,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            handled_by BIGINT(20) UNSIGNED DEFAULT NULL,
            handled_at DATETIME DEFAULT NULL,
            PRIMARY KEY (id),
            KEY idx_plc_events_scope_line (company_id, location_id, production_line_no, event_status)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    } else {
        botella_add_col($pdo, 'production_plc_signal_events', 'tag_code', "tag_code VARCHAR(80) DEFAULT NULL AFTER tag_id");
        botella_add_col($pdo, 'production_plc_signal_events', 'production_line_id', "production_line_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER tag_code");
        botella_add_col($pdo, 'production_plc_signal_events', 'production_line_no', "production_line_no TINYINT UNSIGNED DEFAULT NULL AFTER production_line_id");
        botella_add_col($pdo, 'production_plc_signal_events', 'event_type', "event_type VARCHAR(40) DEFAULT NULL AFTER signal_value");
        botella_add_col($pdo, 'production_plc_signal_events', 'handled_by', "handled_by BIGINT(20) UNSIGNED DEFAULT NULL AFTER processed_by");
        botella_add_col($pdo, 'production_plc_signal_events', 'handled_at', "handled_at DATETIME DEFAULT NULL AFTER handled_by");
        botella_add_index($pdo, 'production_plc_signal_events', 'idx_plc_events_scope_line', "INDEX idx_plc_events_scope_line (company_id, location_id, production_line_no, event_status)");
    }

    $st = $pdo->prepare("SELECT COUNT(*) FROM production_lines WHERE company_id=? AND location_id=?");
    $st->execute([$companyId, $locationId]);
    if ((int)$st->fetchColumn() === 0) {
        $ins = $pdo->prepare("INSERT INTO production_lines(company_id, location_id, line_no, line_code, line_name, active, sort_order, created_by, created_at) VALUES(?,?,?,?,?,1,?,?,NOW())");
        for ($i=1; $i<=3; $i++) {
            $ins->execute([$companyId, $locationId, $i, 'L' . $i, 'Línea ' . $i, $i, $userId]);
        }
    }
}

function botella_signal_settings(PDO $pdo, int $companyId, int $locationId): array {
    if (!botella_table_exists($pdo, 'production_signal_settings')) {
        return ['report_mode'=>'MANUAL','plc_enabled'=>0,'poll_seconds'=>10];
    }
    $st = $pdo->prepare("SELECT * FROM production_signal_settings WHERE company_id=? AND location_id=? LIMIT 1");
    $st->execute([$companyId, $locationId]);
    $row = $st->fetch(PDO::FETCH_ASSOC) ?: [];
    $mode = strtoupper(trim((string)($row['report_mode'] ?? 'MANUAL')));
    $enabled = (int)($row['plc_enabled'] ?? 0);
    return [
        'report_mode' => ($enabled === 1 || $mode === 'AUTOMATIC') ? 'AUTOMATIC' : 'MANUAL',
        'plc_enabled' => ($enabled === 1 || $mode === 'AUTOMATIC') ? 1 : 0,
        'poll_seconds' => max(5, (int)($row['poll_seconds'] ?? 10)),
        'default_product_id' => (int)($row['default_product_id'] ?? 0),
        'default_warehouse_id' => (int)($row['default_warehouse_id'] ?? 0),
        'default_packaging_option_id' => (int)($row['default_packaging_option_id'] ?? 0),
    ];
}


function botella_plc_tags_for_line(PDO $pdo, int $companyId, int $locationId, int $lineNo): array {
    if (!botella_table_exists($pdo, 'production_plc_tags') || $lineNo <= 0) return [];
    $st = $pdo->prepare("SELECT t.id, t.tag_code, t.tag_name, t.line_name, t.production_line_no,
            t.product_id, t.packaging_option_id,
            COALESCE(t.warehouse_id,t.cf_wh_id,0) AS warehouse_id,
            p.sku, p.name AS product_name,
            w.code AS warehouse_code, w.name AS warehouse_name
        FROM production_plc_tags t
        LEFT JOIN products p ON p.id=t.product_id AND p.company_id=t.company_id
        LEFT JOIN warehouses w ON w.id=COALESCE(t.warehouse_id,t.cf_wh_id)
        WHERE t.company_id=?
          AND t.location_id=?
          AND COALESCE(t.active,1)=1
          AND COALESCE(t.enabled,1)=1
          AND (
                t.production_line_no=?
                OR (t.production_line_no IS NULL AND t.line_name LIKE ?)
          )
        ORDER BY t.id");
    $st->execute([$companyId, $locationId, $lineNo, '%' . $lineNo . '%']);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}

function botella_validate_plc_tag_for_line(PDO $pdo, int $companyId, int $locationId, int $tagId, int $lineNo): array {
    if ($tagId <= 0) throw new RuntimeException('Selecciona el TAG PLC que se utilizará para esta línea.');
    $st = $pdo->prepare("SELECT * FROM production_plc_tags WHERE id=? AND company_id=? AND location_id=? AND COALESCE(active,1)=1 AND COALESCE(enabled,1)=1 LIMIT 1");
    $st->execute([$tagId, $companyId, $locationId]);
    $tag = $st->fetch(PDO::FETCH_ASSOC);
    if (!$tag) throw new RuntimeException('El TAG PLC seleccionado no existe o está inactivo.');
    $tagLine = (int)($tag['production_line_no'] ?? 0);
    if ($tagLine <= 0 && !empty($tag['line_name']) && preg_match('/(\d+)/', (string)$tag['line_name'], $m)) $tagLine = (int)$m[1];
    if ($tagLine !== $lineNo) throw new RuntimeException('El TAG PLC seleccionado no pertenece a la línea seleccionada.');
    return $tag;
}

function botella_get_lines(PDO $pdo, int $companyId, int $locationId): array {
    $st = $pdo->prepare("SELECT id, line_no, line_code, line_name, active FROM production_lines WHERE company_id=? AND location_id=? AND active=1 ORDER BY sort_order, line_no");
    $st->execute([$companyId, $locationId]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}

function botella_get_line(PDO $pdo, int $companyId, int $locationId, int $lineId): array {
    $st = $pdo->prepare("SELECT * FROM production_lines WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
    $st->execute([$lineId, $companyId, $locationId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) throw new RuntimeException('Selecciona una línea de producción válida.');
    return $row;
}

function botella_products(PDO $pdo, int $companyId): array {
    $st = $pdo->prepare("SELECT id, sku, name, uom, pieces_per_package, packages_per_pallet FROM products WHERE company_id=? AND active=1 ORDER BY sku, name");
    $st->execute([$companyId]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}

function botella_warehouses(PDO $pdo, int $companyId, int $locationId): array {
    /*
     * Producción Botella solo puede ingresar existencia a almacenes físicos:
     * - ALM01
     * - Cuarentena
     * Nunca debe ingresar producción directo a rutas móviles RUTA-xx.
     */
    $st = $pdo->prepare("
        SELECT id, code, name
        FROM warehouses
        WHERE company_id=?
          AND location_id=?
          AND active=1
          AND UPPER(COALESCE(code,'')) NOT REGEXP '^RUTA[-_ ]?[0-9]+$'
          AND (
                UPPER(COALESCE(code,'')) = 'ALM01'
                OR UPPER(COALESCE(code,'')) IN ('CUARENTENA','CUA','QUA','HLD')
                OR UPPER(COALESCE(name,'')) LIKE '%CUARENTENA%'
          )
        ORDER BY FIELD(UPPER(code),'ALM01','CUARENTENA','CUA','QUA','HLD'), code, name
    ");
    $st->execute([$companyId, $locationId]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}

function botella_default_alm01_warehouse_id(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(COALESCE(code,''))='ALM01' LIMIT 1");
    $st->execute([$companyId, $locationId]);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id > 0) return $id;

    $allowed = botella_warehouses($pdo, $companyId, $locationId);
    if (!empty($allowed[0]['id'])) return (int)$allowed[0]['id'];

    throw new RuntimeException('No existe ALM01 activo para Producción Botella. Configura el almacén ALM01 en esta planta.');
}

function botella_product(PDO $pdo, int $companyId, int $productId): array {
    $st = $pdo->prepare("SELECT id, sku, name, uom, pieces_per_package, packages_per_pallet FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $st->execute([$productId, $companyId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) throw new RuntimeException('Artículo inválido o inactivo.');
    return $row;
}

function botella_packaging(PDO $pdo, int $companyId, int $productId, int $packagingId): array {
    if ($packagingId > 0 && botella_table_exists($pdo, 'product_packaging_options')) {
        $st = $pdo->prepare("SELECT id, product_id, packaging_type, packaging_name, pieces_per_package, packages_per_pallet, is_default, active, 0 AS legacy_fallback FROM product_packaging_options WHERE id=? AND company_id=? AND product_id=? AND active=1 LIMIT 1");
        $st->execute([$packagingId, $companyId, $productId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if ($row) return $row;
    }
    $p = botella_product($pdo, $companyId, $productId);
    if ((int)($p['pieces_per_package'] ?? 0) <= 0 || (int)($p['packages_per_pallet'] ?? 0) <= 0) {
        throw new RuntimeException('El artículo no tiene empaque configurado. Configura product_packaging_options o pieces_per_package/packages_per_pallet en products.');
    }
    return [
        'id' => 0,
        'product_id' => $productId,
        'packaging_type' => 'TARIMA',
        'packaging_name' => 'Embalaje principal · ' . (int)$p['packages_per_pallet'] . ' paquetes/tarima',
        'pieces_per_package' => (int)$p['pieces_per_package'],
        'packages_per_pallet' => (int)$p['packages_per_pallet'],
        'is_default' => 1,
        'legacy_fallback' => 1,
    ];
}

function botella_packaging_options(PDO $pdo, int $companyId, int $productId): array {
    $rows = [];
    if (botella_table_exists($pdo, 'product_packaging_options')) {
        $st = $pdo->prepare("SELECT id, product_id, packaging_type, packaging_name, pieces_per_package, packages_per_pallet, is_default, 0 AS legacy_fallback, (pieces_per_package * packages_per_pallet) AS pieces_per_pallet FROM product_packaging_options WHERE company_id=? AND product_id=? AND active=1 ORDER BY is_default DESC, id");
        $st->execute([$companyId, $productId]);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);
    }
    if (!$rows) {
        $p = botella_product($pdo, $companyId, $productId);
        if ((int)($p['pieces_per_package'] ?? 0) > 0 && (int)($p['packages_per_pallet'] ?? 0) > 0) {
            $rows[] = [
                'id'=>0,
                'product_id'=>$productId,
                'packaging_type'=>'TARIMA',
                'packaging_name'=>'Embalaje principal · ' . (int)$p['packages_per_pallet'] . ' paquetes/tarima',
                'pieces_per_package'=>(int)$p['pieces_per_package'],
                'packages_per_pallet'=>(int)$p['packages_per_pallet'],
                'is_default'=>1,
                'legacy_fallback'=>1,
                'pieces_per_pallet'=>(int)$p['pieces_per_package'] * (int)$p['packages_per_pallet'],
            ];
        }
    }
    return $rows;
}

function botella_validate_int_qty(float $v, string $label): int {
    if ($v < 0) throw new RuntimeException($label . ' no permite cantidades negativas.');
    if (abs($v - round($v)) > 0.000001) throw new RuntimeException($label . ' debe ser entero. No se permiten decimales.');
    return (int)round($v);
}

function botella_calculate(PDO $pdo, int $companyId, int $productId, int $packagingId, float $palletsRaw, float $loosePackagesRaw, float $loosePiecesRaw): array {
    $pack = botella_packaging($pdo, $companyId, $productId, $packagingId);
    $pallets = botella_validate_int_qty($palletsRaw, 'Tarimas/Racks');
    $loosePackages = botella_validate_int_qty($loosePackagesRaw, 'Paquetes sueltos');
    $loosePieces = botella_validate_int_qty($loosePiecesRaw, 'Piezas sueltas');
    if ($pallets <= 0 && $loosePackages <= 0 && $loosePieces <= 0) throw new RuntimeException('Captura tarimas, paquetes sueltos o piezas sueltas.');
    $packagesPerPallet = (int)$pack['packages_per_pallet'];
    $piecesPerPackage = (int)$pack['pieces_per_package'];
    if ($packagesPerPallet <= 0 || $piecesPerPackage <= 0) throw new RuntimeException('El empaque no es válido.');
    $packages = ($pallets * $packagesPerPallet) + $loosePackages;
    $pieces = ($packages * $piecesPerPackage) + $loosePieces;
    if ($pieces <= 0) throw new RuntimeException('El total de piezas calculado debe ser mayor a cero.');
    return [
        'packaging'=>$pack,
        'pallets_qty'=>$pallets,
        'packages_qty'=>$packages,
        'loose_packages'=>$loosePackages,
        'loose_pieces'=>$loosePieces,
        'total_pieces'=>$pieces,
        'pieces_per_package'=>$piecesPerPackage,
        'packages_per_pallet'=>$packagesPerPallet,
    ];
}

function botella_validate_warehouse(PDO $pdo, int $companyId, int $locationId, int $warehouseId): array {
    $st = $pdo->prepare("SELECT id, code, name FROM warehouses WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
    $st->execute([$warehouseId, $companyId, $locationId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) throw new RuntimeException('Almacén destino inválido.');
    $code = strtoupper(trim((string)$row['code']));
    if (in_array($code, ['MERMA','APROV-MERMA','SIN-WH'], true)) throw new RuntimeException('No puedes ingresar producción a almacén de merma/disposición.');
    if (preg_match('/^RUTA[-_ ]?[0-9]+$/i', $code)) {
        throw new RuntimeException('Producción Botella no puede ingresar directo a rutas. Usa ALM01 o Cuarentena.');
    }
    $nameUpper = mb_strtoupper((string)($row['name'] ?? ''), 'UTF-8');
    $isAllowedProductionWh = (
        $code === 'ALM01'
        || in_array($code, ['CUARENTENA','CUA','QUA','HLD'], true)
        || str_contains($nameUpper, 'CUARENTENA')
    );
    if (!$isAllowedProductionWh) {
        throw new RuntimeException('Producción Botella solo permite ingreso a ALM01 o Cuarentena.');
    }
    return $row;
}

function botella_active_line_session(PDO $pdo, int $companyId, int $locationId, int $lineNo): ?array {
    if (botella_col_exists($pdo, 'shift_sessions', 'production_line_no')) {
        $st = $pdo->prepare("SELECT * FROM shift_sessions WHERE company_id=? AND location_id=? AND production_line_no=? AND status='RUNNING' ORDER BY id DESC LIMIT 1");
        $st->execute([$companyId, $locationId, $lineNo]);
    } else {
        $st = $pdo->prepare("SELECT * FROM shift_sessions WHERE company_id=? AND location_id=? AND status='RUNNING' ORDER BY id DESC LIMIT 1");
        $st->execute([$companyId, $locationId]);
    }
    $row = $st->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
}

function botella_get_session(PDO $pdo, int $companyId, int $locationId, int $sessionId): array {
    $st = $pdo->prepare("SELECT * FROM shift_sessions WHERE id=? AND company_id=? AND location_id=? LIMIT 1");
    $st->execute([$sessionId, $companyId, $locationId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if (!$row) throw new RuntimeException('Sesión no encontrada.');
    return $row;
}

function botella_make_uid(string $prefix = 'BOT'): string {
    return $prefix . '-' . date('YmdHis') . '-' . bin2hex(random_bytes(4));
}

function botella_next_label_sequence(PDO $pdo, int $companyId, int $locationId, int $productId, string $workDate): int {
    if (!botella_table_exists($pdo, 'production_labels')) return 1;
    try {
        $st = $pdo->prepare("SELECT COUNT(*) FROM production_labels WHERE company_id=? AND location_id=? AND product_id=? AND work_date=?");
        $st->execute([$companyId, $locationId, $productId, $workDate]);
        return ((int)$st->fetchColumn()) + 1;
    } catch (Throwable $e) {
        return 1;
    }
}

function botella_make_label_code(PDO $pdo, int $companyId, int $locationId, int $productId, string $workDate, int $lineNo, int $shift): string {
    /*
     * Formato preparado para lectura en Montacargas:
     * A{id_articulo}-{fecha}-{hora}-L{linea}-T{turno}-{consecutivo}
     * Ejemplo: A18-20260617-143010-L1-T2-0007
     */
    $seq = botella_next_label_sequence($pdo, $companyId, $locationId, $productId, $workDate);
    return 'A' . $productId . '-' . date('Ymd-His') . '-L' . $lineNo . '-T' . $shift . '-' . str_pad((string)$seq, 4, '0', STR_PAD_LEFT);
}

function botella_create_capture(PDO $pdo, int $companyId, int $locationId, int $userId, array $payload): array {
    $workDate = (string)($payload['work_date'] ?? botella_operational_today());
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDate)) $workDate = botella_operational_today();
    $shift = (int)($payload['shift'] ?? botella_shift_now());
    if (!in_array($shift, [1,2,3], true)) throw new RuntimeException('Turno inválido.');
    $lineId = (int)($payload['production_line_id'] ?? 0);
    $line = botella_get_line($pdo, $companyId, $locationId, $lineId);
    $lineNo = (int)$line['line_no'];
    $productId = (int)($payload['product_id'] ?? 0);
    $warehouseId = (int)($payload['warehouse_id'] ?? 0);
    $packagingId = (int)($payload['packaging_option_id'] ?? 0);
    $palletsRaw = (float)($payload['pallets_qty'] ?? 0);
    $loosePackagesRaw = (float)($payload['loose_packages'] ?? 0);
    $loosePiecesRaw = (float)($payload['loose_pieces'] ?? 0);
    $sourceType = strtoupper(trim((string)($payload['source_type'] ?? 'MANUAL')));
    $plcTagId = (int)($payload['plc_tag_id'] ?? 0);
    $notes = trim((string)($payload['notes'] ?? ''));

    botella_product($pdo, $companyId, $productId);
    $wh = botella_validate_warehouse($pdo, $companyId, $locationId, $warehouseId);
    $calc = botella_calculate($pdo, $companyId, $productId, $packagingId, $palletsRaw, $loosePackagesRaw, $loosePiecesRaw);

    $uid = botella_make_uid('BOTCAP');
    $entryTime = botella_now();
    $labelCode = botella_make_label_code($pdo, $companyId, $locationId, $productId, $workDate, $lineNo, $shift);

    $groupId = botella_insert_row($pdo, 'production_capture_groups', [
        'capture_uid'=>$uid,
        'company_id'=>$companyId,
        'location_id'=>$locationId,
        'production_line_id'=>(int)$line['id'],
        'production_line_no'=>$lineNo,
        'work_date'=>$workDate,
        'shift'=>$shift,
        'product_id'=>$productId,
        'packaging_option_id'=>($packagingId > 0 ? $packagingId : null),
        'warehouse_id'=>$warehouseId,
        'entry_time'=>$entryTime,
        'cf_wh_id'=>$warehouseId,
        'qty_total'=>(float)$calc['total_pieces'],
        'qty_cf'=>(float)$calc['total_pieces'],
        'qty_cub'=>0,
        'qty_merma'=>0,
        'pallets_qty'=>(float)$calc['pallets_qty'],
        'packages_qty'=>(float)$calc['packages_qty'],
        'loose_packages'=>(float)$calc['loose_packages'],
        'loose_pieces'=>(float)$calc['loose_pieces'],
        'source_type'=>$sourceType,
        'plc_tag_id'=>($plcTagId > 0 ? $plcTagId : null),
        'created_by'=>$userId,
        'created_at'=>botella_now(),
    ]);

    $entryId = botella_insert_row($pdo, 'production_entries', [
        'company_id'=>$companyId,
        'location_id'=>$locationId,
        'production_line_id'=>(int)$line['id'],
        'production_line_no'=>$lineNo,
        'work_date'=>$workDate,
        'shift'=>$shift,
        'product_id'=>$productId,
        'packaging_option_id'=>($packagingId > 0 ? $packagingId : null),
        'warehouse_id'=>$warehouseId,
        'qty'=>(float)$calc['total_pieces'],
        'pallets_qty'=>(float)$calc['pallets_qty'],
        'packages_qty'=>(float)$calc['packages_qty'],
        'loose_packages'=>(float)$calc['loose_packages'],
        'loose_pieces'=>(float)$calc['loose_pieces'],
        'entry_time'=>$entryTime,
        'capture_uid'=>$uid,
        'created_by'=>$userId,
        'created_at'=>botella_now(),
    ]);

    $moveNotes = 'Producción Botella ' . $sourceType . ' | Línea ' . $lineNo . ' | ' . $calc['pallets_qty'] . ' tarimas | ' . $calc['packages_qty'] . ' paquetes | ' . $calc['total_pieces'] . ' piezas | Etiqueta ' . $labelCode;
    if ($notes !== '') $moveNotes .= ' | ' . mb_substr($notes, 0, 180, 'UTF-8');
    $moveId = botella_insert_row($pdo, 'inventory_moves', [
        'company_id'=>$companyId,
        'location_id'=>$locationId,
        'warehouse_id'=>$warehouseId,
        'work_date'=>$workDate,
        'product_id'=>$productId,
        'qty'=>(float)$calc['total_pieces'],
        'move_type'=>'IN',
        'channel_id'=>null,
        'ref_type'=>'PRODUCTION_BOTELLA',
        'ref_id'=>$groupId,
        'notes'=>$moveNotes,
        'created_by'=>$userId,
        'created_at'=>botella_now(),
    ]);

    $labelId = botella_insert_row($pdo, 'production_labels', [
        'company_id'=>$companyId,
        'location_id'=>$locationId,
        'work_date'=>$workDate,
        'shift'=>$shift,
        'production_line_id'=>(int)$line['id'],
        'production_line_no'=>$lineNo,
        'product_id'=>$productId,
        'packaging_option_id'=>($packagingId > 0 ? $packagingId : null),
        'capture_group_id'=>$groupId,
        'inventory_move_id'=>$moveId,
        'label_code'=>$labelCode,
        'pallets_qty'=>(float)$calc['pallets_qty'],
        'packages_qty'=>(float)$calc['packages_qty'],
        'loose_packages'=>(float)$calc['loose_packages'],
        'loose_pieces'=>(float)$calc['loose_pieces'],
        'total_pieces'=>(float)$calc['total_pieces'],
        'status'=>'ACTIVA',
        'created_by'=>$userId,
        'created_at'=>botella_now(),
    ]);

    if (botella_col_exists($pdo, 'production_capture_groups', 'label_id')) {
        $pdo->prepare("UPDATE production_capture_groups SET label_id=? WHERE id=?")->execute([$labelId, $groupId]);
    }
    if (botella_col_exists($pdo, 'production_entries', 'label_id')) {
        $pdo->prepare("UPDATE production_entries SET label_id=? WHERE id=?")->execute([$labelId, $entryId]);
    }

    return [
        'capture_uid'=>$uid,
        'capture_group_id'=>$groupId,
        'entry_id'=>$entryId,
        'inventory_move_id'=>$moveId,
        'label_id'=>$labelId,
        'label_code'=>$labelCode,
        'line_no'=>$lineNo,
        'warehouse'=>$wh,
        'calculation'=>$calc,
    ];
}

function botella_summary(PDO $pdo, int $companyId, int $locationId, string $date): array {
    $st = $pdo->prepare("SELECT COALESCE(SUM(qty),0) FROM inventory_moves WHERE company_id=? AND location_id=? AND work_date=? AND move_type='IN' AND ref_type='PRODUCTION_BOTELLA'");
    $st->execute([$companyId, $locationId, $date]);
    $produced = (float)$st->fetchColumn();
    $byLine = [];
    $lineCol = botella_col_exists($pdo, 'production_capture_groups', 'production_line_no') ? 'production_line_no' : 'shift';
    $st = $pdo->prepare("SELECT {$lineCol} AS line_no, COALESCE(SUM(qty_total),0) AS qty FROM production_capture_groups WHERE company_id=? AND location_id=? AND work_date=? GROUP BY {$lineCol} ORDER BY {$lineCol}");
    $st->execute([$companyId, $locationId, $date]);
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) $byLine[(int)$r['line_no']] = (float)$r['qty'];
    $target = 0.0;
    if (botella_table_exists($pdo, 'production_day_targets')) {
        $st = $pdo->prepare("SELECT COALESCE(SUM(target_pieces),0) FROM production_day_targets WHERE company_id=? AND location_id=? AND work_date=?");
        $st->execute([$companyId, $locationId, $date]);
        $target = (float)$st->fetchColumn();
    }
    return ['produced'=>$produced, 'target'=>$target, 'progress'=>($target > 0 ? ($produced/$target)*100 : 0), 'by_line'=>$byLine];
}

/*
 * Impresión de etiqueta en ventana separada:
 * Si el operador sale del módulo o cambia de pantalla mientras la ventana de impresión
 * sigue abierta, el scope activo de sesión puede cambiar. Para que la etiqueta ya
 * generada no provoque un 403 al recargar/abrir la ventana, resolvemos el scope
 * directamente desde production_labels cuando viene label_id.
 * Esto NO cambia el scope del usuario; solo permite renderizar la etiqueta autenticada.
 */
if (isset($_GET['label_id']) && !isset($_GET['action'])) {
    try {
        $labelScopeId = (int)($_GET['label_id'] ?? 0);
        if ($labelScopeId > 0 && botella_table_exists($pdo, 'production_labels')) {
            $stLabelScope = $pdo->prepare("SELECT company_id, location_id FROM production_labels WHERE id=? LIMIT 1");
            $stLabelScope->execute([$labelScopeId]);
            $labelScope = $stLabelScope->fetch(PDO::FETCH_ASSOC);
            if ($labelScope) {
                $companyId = (int)($labelScope['company_id'] ?? $companyId);
                $locationId = (int)($labelScope['location_id'] ?? $locationId);
            }
        }
    } catch (Throwable $e) {}
}

botella_assert_scope($pdo, $companyId, $locationId);
botella_ensure_schema($pdo, $companyId, $locationId, (int)($u['id'] ?? 0));

if (!botella_can_produce()) {
    http_response_code(403);
    exit('Acceso denegado. Este módulo requiere rol de producción, almacén, supervisor o administrador.');
}

if ($action !== '') {
    botella_need_csrf();
    try {
        if ($action === 'bootstrap') {
            $date = (string)($_GET['work_date'] ?? botella_operational_today());
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) $date = botella_operational_today();
            $lines = botella_get_lines($pdo, $companyId, $locationId);
            $sessions = [];
            $st = $pdo->prepare("SELECT * FROM shift_sessions WHERE company_id=? AND location_id=? AND status='RUNNING' ORDER BY production_line_no, id DESC");
            $st->execute([$companyId, $locationId]);
            $sessions = $st->fetchAll(PDO::FETCH_ASSOC);
            botella_jexit([
                'ok'=>true,
                'today'=>$date,
                'current_shift'=>botella_shift_now(),
                'lines'=>$lines,
                'products'=>botella_products($pdo, $companyId),
                'warehouses'=>botella_warehouses($pdo, $companyId, $locationId),
                'sessions'=>$sessions,
                'summary'=>botella_summary($pdo, $companyId, $locationId, $date),
                'can_admin'=>botella_is_admin(),
                'signal_settings'=>botella_signal_settings($pdo, $companyId, $locationId),
            ]);
        }

        if ($action === 'plc_tags_for_line') {
            $lineId = (int)($_GET['line_id'] ?? 0);
            $line = botella_get_line($pdo, $companyId, $locationId, $lineId);
            botella_jexit(['ok'=>true, 'rows'=>botella_plc_tags_for_line($pdo, $companyId, $locationId, (int)$line['line_no']), 'line'=>$line]);
        }

        if ($action === 'packaging_options') {
            $pid = (int)($_GET['product_id'] ?? 0);
            if ($pid <= 0) throw new RuntimeException('Artículo requerido.');
            botella_jexit(['ok'=>true, 'rows'=>botella_packaging_options($pdo, $companyId, $pid)]);
        }

        if ($action === 'start_shift') {
            $d = botella_read_json();
            $lineId = (int)($d['production_line_id'] ?? 0);
            $shift = (int)($d['shift'] ?? botella_shift_now());
            $workDate = trim((string)($d['work_date'] ?? botella_operational_today()));
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDate)) $workDate = botella_operational_today();
            if (!in_array($shift, [1,2,3], true)) throw new RuntimeException('Turno inválido.');
            $line = botella_get_line($pdo, $companyId, $locationId, $lineId);
            $reportMode = strtoupper(trim((string)($d['report_mode'] ?? 'MANUAL')));
            if (!in_array($reportMode, ['MANUAL','AUTOMATIC'], true)) $reportMode = 'MANUAL';
            $plcTagId = (int)($d['plc_tag_id'] ?? 0);
            if ($reportMode === 'AUTOMATIC') {
                $tags = botella_plc_tags_for_line($pdo, $companyId, $locationId, (int)$line['line_no']);
                if (!$tags) throw new RuntimeException('La ' . $line['line_name'] . ' no tiene TAG PLC activo configurado. Usa Manual o configura production_plc_tags.');
                if ($plcTagId <= 0 && count($tags) === 1) $plcTagId = (int)$tags[0]['id'];
                botella_validate_plc_tag_for_line($pdo, $companyId, $locationId, $plcTagId, (int)$line['line_no']);
            } else {
                $plcTagId = 0;
            }
            $running = botella_active_line_session($pdo, $companyId, $locationId, (int)$line['line_no']);
            if ($running) {
                throw new RuntimeException('La ' . $line['line_name'] . ' ya tiene un turno activo. Primero finaliza la sesión activa #' . (int)$running['id'] . '.');
            }
            $id = botella_insert_row($pdo, 'shift_sessions', [
                'company_id'=>$companyId,
                'location_id'=>$locationId,
                'production_line_id'=>(int)$line['id'],
                'production_line_no'=>(int)$line['line_no'],
                'line_name'=>(string)$line['line_name'],
                'user_id'=>(int)$u['id'],
                'work_date'=>$workDate,
                'shift'=>$shift,
                'start_at'=>botella_now(),
                'status'=>'RUNNING',
                'report_mode'=>$reportMode,
                'plc_tag_id'=>($plcTagId > 0 ? $plcTagId : null),
                'last_heartbeat_at'=>botella_now(),
                'created_at'=>botella_now(),
            ]);
            botella_jexit(['ok'=>true, 'session_id'=>$id, 'message'=>'Turno iniciado en ' . $line['line_name'] . ' en modo ' . ($reportMode === 'AUTOMATIC' ? 'AUTOMÁTICO PLC' : 'MANUAL')]);
        }

        if ($action === 'end_shift') {
            $d = botella_read_json();
            $sessionId = (int)($d['session_id'] ?? 0);
            $session = botella_get_session($pdo, $companyId, $locationId, $sessionId);
            if (($session['status'] ?? '') !== 'RUNNING') throw new RuntimeException('La sesión ya está finalizada.');
            $createdCapture = null;
            $loosePackages = (float)($d['loose_packages'] ?? 0);
            $loosePieces = (float)($d['loose_pieces'] ?? 0);
            /*
             * Última tarima:
             * Solo al cierre de turno se permite registrar paquetes/piezas sueltas.
             * Esto genera IN real y etiqueta, pero se identifica como ULTIMA_TARIMA.
             */
            if ($loosePackages > 0 || $loosePieces > 0) {
                $payload = [
                    'work_date'=>$session['work_date'],
                    'shift'=>(int)$session['shift'],
                    'production_line_id'=>(int)($session['production_line_id'] ?? 0),
                    'product_id'=>(int)($d['product_id'] ?? 0),
                    'warehouse_id'=>((int)($d['warehouse_id'] ?? 0) > 0 ? (int)($d['warehouse_id'] ?? 0) : botella_default_alm01_warehouse_id($pdo, $companyId, $locationId)),
                    'packaging_option_id'=>(int)($d['packaging_option_id'] ?? 0),
                    'pallets_qty'=>0,
                    'loose_packages'=>$loosePackages,
                    'loose_pieces'=>$loosePieces,
                    'source_type'=>'ULTIMA_TARIMA',
                    'notes'=>'Última tarima de cierre de turno: paquetes/piezas sueltas',
                ];
                $createdCapture = botella_create_capture($pdo, $companyId, $locationId, (int)$u['id'], $payload);
            }
            $notes = trim((string)($d['notes'] ?? ''));
            if (botella_col_exists($pdo, 'shift_sessions', 'end_notes')) {
                $st = $pdo->prepare("UPDATE shift_sessions SET status='ENDED', end_at=NOW(), last_heartbeat_at=NOW(), end_notes=? WHERE id=? AND company_id=? AND location_id=?");
                $st->execute([$notes !== '' ? $notes : null, $sessionId, $companyId, $locationId]);
            } else {
                $st = $pdo->prepare("UPDATE shift_sessions SET status='ENDED', end_at=NOW(), last_heartbeat_at=NOW() WHERE id=? AND company_id=? AND location_id=?");
                $st->execute([$sessionId, $companyId, $locationId]);
            }
            $reportUrl = 'Produccion_botella.php?report=shift&work_date=' . rawurlencode((string)$session['work_date']) . '&shift=' . (int)$session['shift'];
            botella_jexit(['ok'=>true, 'capture'=>$createdCapture, 'report_url'=>$reportUrl]);
        }

        if ($action === 'manual_capture') {
            $d = botella_read_json();
            $sessionId = (int)($d['session_id'] ?? 0);
            $session = botella_get_session($pdo, $companyId, $locationId, $sessionId);
            if (($session['status'] ?? '') !== 'RUNNING') throw new RuntimeException('No puedes capturar sobre una sesión finalizada.');
            $payload = [
                'work_date'=>$session['work_date'],
                'shift'=>(int)$session['shift'],
                'production_line_id'=>(int)($session['production_line_id'] ?? 0),
                'product_id'=>(int)($d['product_id'] ?? 0),
                // Producción normal siempre ingresa automáticamente a ALM01.
                'warehouse_id'=>botella_default_alm01_warehouse_id($pdo, $companyId, $locationId),
                'packaging_option_id'=>(int)($d['packaging_option_id'] ?? 0),
                // Producción normal Botella siempre reporta 1 tarima completa.
                // Paquetes/piezas sueltas quedan reservados para "Última tarima" al cierre de turno.
                'pallets_qty'=>1,
                'loose_packages'=>0,
                'loose_pieces'=>0,
                'source_type'=>'MANUAL',
                'notes'=>(string)($d['notes'] ?? ''),
            ];
            $pdo->beginTransaction();
            $cap = botella_create_capture($pdo, $companyId, $locationId, (int)$u['id'], $payload);
            $pdo->commit();
            botella_jexit(['ok'=>true, 'capture'=>$cap]);
        }

        if ($action === 'set_target') {
            if (!botella_is_admin()) throw new RuntimeException('Solo admin/supervisor puede modificar objetivo del día.');
            $d = botella_read_json();
            $workDate = trim((string)($d['work_date'] ?? botella_operational_today()));
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDate)) $workDate = botella_operational_today();
            $target = (float)($d['target_pieces'] ?? 0);
            if ($target < 0) throw new RuntimeException('El objetivo no puede ser negativo.');
            $notes = trim((string)($d['notes'] ?? ''));
            $st = $pdo->prepare("SELECT id FROM production_day_targets WHERE company_id=? AND location_id=? AND work_date=? AND product_id IS NULL ORDER BY id DESC LIMIT 1");
            $st->execute([$companyId, $locationId, $workDate]);
            $id = (int)($st->fetchColumn() ?: 0);
            if ($id > 0) {
                $st = $pdo->prepare("UPDATE production_day_targets SET target_pieces=?, notes=?, updated_by=?, updated_at=NOW() WHERE id=?");
                $st->execute([$target, $notes !== '' ? $notes : null, (int)$u['id'], $id]);
            } else {
                botella_insert_row($pdo, 'production_day_targets', [
                    'company_id'=>$companyId,
                    'location_id'=>$locationId,
                    'work_date'=>$workDate,
                    'product_id'=>null,
                    'target_pieces'=>$target,
                    'notes'=>$notes !== '' ? $notes : null,
                    'created_by'=>(int)$u['id'],
                    'created_at'=>botella_now(),
                ]);
            }
            botella_jexit(['ok'=>true]);
        }

        if ($action === 'captures') {
            $date = (string)($_GET['work_date'] ?? botella_operational_today());
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) $date = botella_operational_today();

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

            $limit = (int)($_GET['limit'] ?? 300);
            $limit = max(1, min(500, $limit));

            $st = $pdo->prepare("SELECT cg.*, p.sku, p.name AS product_name, w.code AS warehouse_code, w.name AS warehouse_name, pl.label_code, pl.id AS label_id, pl.printed_at
                FROM production_capture_groups cg
                JOIN products p ON p.id=cg.product_id
                JOIN warehouses w ON w.id=COALESCE(cg.warehouse_id,cg.cf_wh_id)
                LEFT JOIN production_labels pl ON pl.capture_group_id=cg.id
                WHERE cg.company_id=? AND cg.location_id=? AND cg.work_date BETWEEN ? AND ?
                ORDER BY cg.id DESC LIMIT {$limit}");
            $st->execute([$companyId, $locationId, $dateFrom, $dateTo]);
            botella_jexit(['ok'=>true, 'rows'=>$st->fetchAll(PDO::FETCH_ASSOC), 'date_from'=>$dateFrom, 'date_to'=>$dateTo]);
        }

        if ($action === 'signal_settings') {
            botella_jexit(['ok'=>true, 'settings'=>botella_signal_settings($pdo, $companyId, $locationId)]);
        }

        if ($action === 'save_signal_settings') {
            if (!botella_is_admin()) throw new RuntimeException('Solo admin/supervisor puede cambiar la configuración PLC.');
            $d = botella_read_json();
            $enabled = !empty($d['plc_enabled']) ? 1 : 0;
            $mode = $enabled ? 'AUTOMATIC' : 'MANUAL';
            $poll = max(5, min(120, (int)($d['poll_seconds'] ?? 10)));
            $st = $pdo->prepare("SELECT id FROM production_signal_settings WHERE company_id=? AND location_id=? LIMIT 1");
            $st->execute([$companyId, $locationId]);
            $id = (int)($st->fetchColumn() ?: 0);
            if ($id > 0) {
                $st = $pdo->prepare("UPDATE production_signal_settings SET report_mode=?, plc_enabled=?, poll_seconds=?, updated_by=?, updated_at=NOW() WHERE id=? AND company_id=? AND location_id=?");
                $st->execute([$mode, $enabled, $poll, (int)$u['id'], $id, $companyId, $locationId]);
            } else {
                botella_insert_row($pdo, 'production_signal_settings', [
                    'company_id'=>$companyId,
                    'location_id'=>$locationId,
                    'report_mode'=>$mode,
                    'plc_enabled'=>$enabled,
                    'poll_seconds'=>$poll,
                    'updated_by'=>(int)$u['id'],
                    'created_at'=>botella_now(),
                    'updated_at'=>botella_now(),
                ]);
            }
            botella_jexit(['ok'=>true, 'settings'=>botella_signal_settings($pdo, $companyId, $locationId)]);
        }


        if ($action === 'switch_session_to_automatic') {
            $d = botella_read_json();
            $sessionId = (int)($d['session_id'] ?? 0);
            $plcTagId = (int)($d['plc_tag_id'] ?? 0);
            $code = trim((string)($d['auth_code'] ?? ''));
            if ($code !== '5321') {
                throw new RuntimeException('Código de autorización incorrecto. No se cambió la sesión a automático.');
            }
            if ($sessionId <= 0) throw new RuntimeException('Selecciona la sesión/línea que se cambiará a automático.');
            $session = botella_get_session($pdo, $companyId, $locationId, $sessionId);
            if (($session['status'] ?? '') !== 'RUNNING') throw new RuntimeException('La sesión seleccionada no está activa.');
            $lineNo = (int)($session['production_line_no'] ?? 0);
            if ($lineNo <= 0) throw new RuntimeException('La sesión no tiene línea de producción asignada.');
            $tag = botella_validate_plc_tag_for_line($pdo, $companyId, $locationId, $plcTagId, $lineNo);

            $pdo->beginTransaction();
            try {
                $st = $pdo->prepare("UPDATE shift_sessions
                    SET report_mode='AUTOMATIC', plc_tag_id=?, last_heartbeat_at=NOW()
                    WHERE id=? AND company_id=? AND location_id=? AND status='RUNNING'");
                $st->execute([$plcTagId, $sessionId, $companyId, $locationId]);

                if (botella_table_exists($pdo, 'production_signal_settings')) {
                    $stSet = $pdo->prepare("SELECT id FROM production_signal_settings WHERE company_id=? AND location_id=? LIMIT 1 FOR UPDATE");
                    $stSet->execute([$companyId, $locationId]);
                    $setId = (int)($stSet->fetchColumn() ?: 0);
                    if ($setId > 0) {
                        $upd = $pdo->prepare("UPDATE production_signal_settings
                            SET report_mode='AUTOMATIC', plc_enabled=1, updated_by=?, updated_at=NOW()
                            WHERE id=? AND company_id=? AND location_id=?");
                        $upd->execute([(int)$u['id'], $setId, $companyId, $locationId]);
                    } else {
                        botella_insert_row($pdo, 'production_signal_settings', [
                            'company_id'=>$companyId,
                            'location_id'=>$locationId,
                            'report_mode'=>'AUTOMATIC',
                            'plc_enabled'=>1,
                            'poll_seconds'=>10,
                            'updated_by'=>(int)$u['id'],
                            'created_at'=>botella_now(),
                            'updated_at'=>botella_now(),
                        ]);
                    }
                }
                $pdo->commit();
            } catch (Throwable $e) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                throw $e;
            }

            botella_jexit([
                'ok'=>true,
                'message'=>'Sesión cambiada a AUTOMÁTICO PLC. La línea ahora escuchará el TAG ' . (string)($tag['tag_code'] ?? ('#'.$plcTagId)) . '.',
                'session_id'=>$sessionId,
                'plc_tag_id'=>$plcTagId,
                'tag_code'=>(string)($tag['tag_code'] ?? ''),
            ]);
        }

        if ($action === 'switch_session_to_manual') {
            $d = botella_read_json();
            $sessionId = (int)($d['session_id'] ?? 0);
            $code = trim((string)($d['auth_code'] ?? ''));
            if ($code !== '2304') {
                throw new RuntimeException('Código de autorización incorrecto. No se cambió la sesión a manual.');
            }
            if ($sessionId <= 0) throw new RuntimeException('Selecciona la sesión/línea que se cambiará a manual.');
            $session = botella_get_session($pdo, $companyId, $locationId, $sessionId);
            if (($session['status'] ?? '') !== 'RUNNING') throw new RuntimeException('La sesión seleccionada no está activa.');
            if (strtoupper((string)($session['report_mode'] ?? 'MANUAL')) !== 'AUTOMATIC') {
                throw new RuntimeException('La sesión seleccionada ya está en modo manual.');
            }

            $pdo->beginTransaction();
            try {
                $st = $pdo->prepare("UPDATE shift_sessions
                    SET report_mode='MANUAL', plc_tag_id=NULL, last_heartbeat_at=NOW()
                    WHERE id=? AND company_id=? AND location_id=? AND status='RUNNING'");
                $st->execute([$sessionId, $companyId, $locationId]);

                $stAuto = $pdo->prepare("SELECT COUNT(*)
                    FROM shift_sessions
                    WHERE company_id=? AND location_id=? AND status='RUNNING' AND COALESCE(report_mode,'MANUAL')='AUTOMATIC'");
                $stAuto->execute([$companyId, $locationId]);
                $remainingAutomatic = (int)$stAuto->fetchColumn();

                if ($remainingAutomatic <= 0 && botella_table_exists($pdo, 'production_signal_settings')) {
                    $stSet = $pdo->prepare("SELECT id FROM production_signal_settings WHERE company_id=? AND location_id=? LIMIT 1 FOR UPDATE");
                    $stSet->execute([$companyId, $locationId]);
                    $setId = (int)($stSet->fetchColumn() ?: 0);
                    if ($setId > 0) {
                        $upd = $pdo->prepare("UPDATE production_signal_settings
                            SET report_mode='MANUAL', plc_enabled=0, updated_by=?, updated_at=NOW()
                            WHERE id=? AND company_id=? AND location_id=?");
                        $upd->execute([(int)$u['id'], $setId, $companyId, $locationId]);
                    }
                }
                $pdo->commit();
            } catch (Throwable $e) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                throw $e;
            }

            botella_jexit([
                'ok'=>true,
                'message'=>'Sesión cambiada a MANUAL. La línea dejó de escuchar señales PLC para esta sesión.',
                'session_id'=>$sessionId,
            ]);
        }


        if ($action === 'plc_pending') {
            $settings = botella_signal_settings($pdo, $companyId, $locationId);
            if ((int)($settings['plc_enabled'] ?? 0) !== 1) {
                botella_jexit(['ok'=>true, 'rows'=>[], 'manual_mode'=>true, 'settings'=>$settings]);
            }
            $rows = [];
            if (botella_table_exists($pdo, 'production_plc_tags')) {
                $st = $pdo->prepare("SELECT t.*, p.sku, p.name AS product_name, w.code AS warehouse_code, w.name AS warehouse_name, l.line_name AS configured_line_name
                    FROM production_plc_tags t
                    LEFT JOIN products p ON p.id=t.product_id
                    LEFT JOIN warehouses w ON w.id=COALESCE(t.warehouse_id,t.cf_wh_id)
                    LEFT JOIN production_lines l ON l.company_id=t.company_id AND l.location_id=t.location_id AND l.line_no=t.production_line_no
                    WHERE t.company_id=?
                      AND t.location_id=?
                      AND COALESCE(t.active,1)=1
                      AND COALESCE(t.enabled,1)=1
                      AND COALESCE(t.signal_state,0)=1
                      AND EXISTS (
                          SELECT 1
                          FROM shift_sessions ss
                          WHERE ss.company_id=t.company_id
                            AND ss.location_id=t.location_id
                            AND ss.production_line_no=t.production_line_no
                            AND ss.status='RUNNING'
                            AND COALESCE(ss.report_mode,'MANUAL')='AUTOMATIC'
                            AND (COALESCE(ss.plc_tag_id,0)=0 OR ss.plc_tag_id=t.id)
                      )
                    ORDER BY t.production_line_no, t.id");
                $st->execute([$companyId, $locationId]);
                $rows = $st->fetchAll(PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    try {
                        $exists = false;
                        if (botella_table_exists($pdo, 'production_plc_signal_events')) {
                            $stE = $pdo->prepare("SELECT id FROM production_plc_signal_events WHERE company_id=? AND location_id=? AND tag_id=? AND event_status='PENDING' ORDER BY id DESC LIMIT 1");
                            $stE->execute([$companyId, $locationId, (int)$r['id']]);
                            $exists = (bool)$stE->fetchColumn();
                        }
                        if (!$exists) {
                            botella_insert_row($pdo, 'production_plc_signal_events', [
                                'company_id'=>$companyId,
                                'location_id'=>$locationId,
                                'tag_id'=>(int)$r['id'],
                                'tag_code'=>(string)$r['tag_code'],
                                'production_line_id'=>(int)($r['production_line_id'] ?? 0) ?: null,
                                'production_line_no'=>(int)($r['production_line_no'] ?? 0) ?: null,
                                'work_date'=>botella_operational_today(),
                                'shift'=>botella_shift_now(),
                                'signal_value'=>1,
                                'event_type'=>'RISING_EDGE',
                                'event_status'=>'PENDING',
                                'detected_at'=>botella_now(),
                                'created_by'=>(int)$u['id'],
                                'created_at'=>botella_now(),
                            ]);
                        }
                    } catch (Throwable $e) {}
                }
            }
            botella_jexit(['ok'=>true, 'rows'=>$rows]);
        }

        if ($action === 'plc_confirm_production') {
            $d = botella_read_json();
            $tagId = (int)($d['tag_id'] ?? 0);
            if ($tagId <= 0) throw new RuntimeException('Tag requerido.');
            $st = $pdo->prepare("SELECT * FROM production_plc_tags WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
            $st->execute([$tagId, $companyId, $locationId]);
            $tag = $st->fetch(PDO::FETCH_ASSOC);
            if (!$tag) throw new RuntimeException('Tag PLC no encontrado o inactivo.');
            $lineNo = (int)($tag['production_line_no'] ?? 0);
            if ($lineNo <= 0 && !empty($tag['line_name'])) {
                if (preg_match('/(\d+)/', (string)$tag['line_name'], $m)) $lineNo = (int)$m[1];
            }
            if ($lineNo <= 0) throw new RuntimeException('El tag no tiene línea configurada.');
            $session = botella_active_line_session($pdo, $companyId, $locationId, $lineNo);
            if (!$session) throw new RuntimeException('La línea del tag no tiene turno activo. Inicia turno antes de confirmar producción PLC.');
            if (strtoupper((string)($session['report_mode'] ?? 'MANUAL')) !== 'AUTOMATIC') {
                throw new RuntimeException('La línea está iniciada en modo MANUAL. Para recibir señales PLC, finaliza e inicia turno en modo AUTOMÁTICO.');
            }
            $selectedTagId = (int)($session['plc_tag_id'] ?? 0);
            if ($selectedTagId > 0 && $selectedTagId !== $tagId) {
                throw new RuntimeException('Este TAG no es el seleccionado para la sesión activa de la línea.');
            }
            $lineId = (int)($session['production_line_id'] ?? 0);

            /*
             * BOTELLA · PRODUCTO DINÁMICO POR SELECCIÓN DE PANTALLA
             * -----------------------------------------------------
             * La misma señal/tag PLC puede reportar diferentes artículos.
             * Por eso NO obligamos a que production_plc_tags tenga product_id,
             * packaging_option_id o warehouse_id configurados.
             *
             * La pantalla manda el artículo y embalaje actualmente seleccionados
             * en captura manual. El almacén destino se recibe desde pantalla o,
             * si no viene, se toma ALM01 automáticamente.
             */
            $productId = (int)($d['product_id'] ?? 0);
            $packagingId = (int)($d['packaging_option_id'] ?? 0);
            $warehouseId = (int)($d['warehouse_id'] ?? 0);

            if ($productId <= 0) {
                throw new RuntimeException('Selecciona el artículo activo antes de procesar la señal PLC. La tag es por sensor, no por artículo.');
            }
            if ($warehouseId <= 0) {
                $warehouseId = botella_default_alm01_warehouse_id($pdo, $companyId, $locationId);
            }

            // Validación temprana para mostrar errores claros antes de crear movimientos.
            botella_product($pdo, $companyId, $productId);
            botella_validate_warehouse($pdo, $companyId, $locationId, $warehouseId);
            botella_packaging($pdo, $companyId, $productId, $packagingId);

            $pallets = 1;
            $pdo->beginTransaction();
            $cap = botella_create_capture($pdo, $companyId, $locationId, (int)$u['id'], [
                'work_date'=>$session['work_date'],
                'shift'=>(int)$session['shift'],
                'production_line_id'=>$lineId,
                'product_id'=>$productId,
                'warehouse_id'=>$warehouseId,
                'packaging_option_id'=>$packagingId,
                // Cada señal PLC válida representa 1 tarima completa de producción Botella.
                'pallets_qty'=>1,
                'loose_packages'=>0,
                'loose_pieces'=>0,
                'source_type'=>'PLC',
                'plc_tag_id'=>$tagId,
                'notes'=>'Confirmación por señal PLC ' . (string)$tag['tag_code'] . ' | artículo/embalaje tomado de selección activa en pantalla',
            ]);
            $stE = $pdo->prepare("UPDATE production_plc_signal_events SET event_status='PROCESSED', processed_at=NOW(), processed_by=?, handled_at=NOW(), handled_by=?, production_capture_uid=? WHERE company_id=? AND location_id=? AND tag_id=? AND event_status='PENDING'");
            $stE->execute([(int)$u['id'], (int)$u['id'], $cap['capture_uid'], $companyId, $locationId, $tagId]);
            $stT = $pdo->prepare("UPDATE production_plc_tags SET signal_state=0, last_signal_state=1, status='RESOLVED', handled_at=NOW(), handled_by=?, last_action='PRODUCTION', consumed_at=NOW(), updated_at=NOW() WHERE id=? AND company_id=? AND location_id=?");
            $stT->execute([(int)$u['id'], $tagId, $companyId, $locationId]);
            $pdo->commit();
            botella_jexit(['ok'=>true, 'capture'=>$cap]);
        }

        if ($action === 'plc_maintenance') {
            $d = botella_read_json();
            $tagId = (int)($d['tag_id'] ?? 0);
            $notes = trim((string)($d['notes'] ?? 'Mantenimiento / señal sin producción'));
            if ($tagId <= 0) throw new RuntimeException('Tag requerido.');
            $st = $pdo->prepare("UPDATE production_plc_signal_events SET event_status='MAINTENANCE', processed_at=NOW(), processed_by=?, handled_at=NOW(), handled_by=?, notes=? WHERE company_id=? AND location_id=? AND tag_id=? AND event_status='PENDING'");
            $st->execute([(int)$u['id'], (int)$u['id'], $notes, $companyId, $locationId, $tagId]);
            $st = $pdo->prepare("UPDATE production_plc_tags SET signal_state=0, last_signal_state=1, status='RESOLVED', handled_at=NOW(), handled_by=?, last_action='MAINTENANCE', notes=?, updated_at=NOW() WHERE id=? AND company_id=? AND location_id=?");
            $st->execute([(int)$u['id'], $notes, $tagId, $companyId, $locationId]);
            botella_jexit(['ok'=>true]);
        }

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


function botella_code128b_svg(string $text, int $height = 58): string {
    /*
     * Generador local de código de barras CODE-128B.
     * No depende de librerías externas ni internet; ideal para scanner USB/BT.
     */
    $patterns = [
        '212222','222122','222221','121223','121322','131222','122213','122312','132212','221213',
        '221312','231212','112232','122132','122231','113222','123122','123221','223211','221132',
        '221231','213212','223112','312131','311222','321122','321221','312212','322112','322211',
        '212123','212321','232121','111323','131123','131321','112313','132113','132311','211313',
        '231113','231311','112133','112331','132131','113123','113321','133121','313121','211331',
        '231131','213113','213311','213131','311123','311321','331121','312113','312311','332111',
        '314111','221411','431111','111224','111422','121124','121421','141122','141221','112214',
        '112412','122114','122411','142112','142211','241211','221114','413111','241112','134111',
        '111242','121142','121241','114212','124112','124211','411212','421112','421211','212141',
        '214121','412121','111143','111341','131141','114113','114311','411113','411311','113141',
        '114131','311141','411131','211412','211214','211232','2331112'
    ];
    $text = trim($text);
    if ($text === '') $text = 'SIN-CODIGO';
    $values = [104]; // Start Code B
    $checksum = 104;
    $pos = 1;
    $len = strlen($text);
    for ($i = 0; $i < $len; $i++) {
        $ord = ord($text[$i]);
        if ($ord < 32 || $ord > 127) continue;
        $v = $ord - 32;
        $values[] = $v;
        $checksum += $v * $pos;
        $pos++;
    }
    $values[] = $checksum % 103;
    $values[] = 106; // Stop

    $quiet = 10;
    $module = 1;
    $x = $quiet;
    $rects = [];
    foreach ($values as $v) {
        $pattern = $patterns[$v] ?? '';
        $black = true;
        foreach (str_split($pattern) as $wChar) {
            $w = (int)$wChar;
            if ($black) {
                $rects[] = '<rect x="' . $x . '" y="0" width="' . ($w * $module) . '" height="' . $height . '" />';
            }
            $x += $w * $module;
            $black = !$black;
        }
    }
    $width = $x + $quiet;
    return '<svg class="barcode-svg" viewBox="0 0 ' . $width . ' ' . $height . '" preserveAspectRatio="none" role="img" aria-label="' . h($text) . '">' . implode('', $rects) . '</svg>';
}


function botella_qr_gf_mul(int $x, int $y): int {
    $r = 0;
    while ($y > 0) {
        if ($y & 1) $r ^= $x;
        $x <<= 1;
        if ($x & 0x100) $x ^= 0x11d;
        $y >>= 1;
    }
    return $r & 0xff;
}

function botella_qr_gf_pow(int $x, int $power): int {
    $r = 1;
    for ($i = 0; $i < $power; $i++) $r = botella_qr_gf_mul($r, $x);
    return $r;
}

function botella_qr_generator_poly(int $degree): array {
    // Divisor Reed-Solomon en el mismo orden usado por QR Code.
    $result = array_fill(0, $degree, 0);
    $result[$degree - 1] = 1;
    for ($i = 0; $i < $degree; $i++) {
        $root = botella_qr_gf_pow(2, $i);
        for ($j = 0; $j < $degree; $j++) {
            $result[$j] = botella_qr_gf_mul($result[$j], $root);
            if ($j + 1 < $degree) $result[$j] ^= $result[$j + 1];
        }
    }
    return $result;
}

function botella_qr_rs_ecc(array $data, int $eccLen): array {
    $divisor = botella_qr_generator_poly($eccLen);
    $ecc = array_fill(0, $eccLen, 0);
    foreach ($data as $b) {
        $factor = $b ^ $ecc[0];
        array_shift($ecc);
        $ecc[] = 0;
        for ($i = 0; $i < $eccLen; $i++) {
            $ecc[$i] ^= botella_qr_gf_mul($divisor[$i], $factor);
        }
    }
    return $ecc;
}

function botella_qr_bits_to_codewords(array $bits, int $dataCodewords): array {
    // Terminador y padding para QR versión 5-L: 108 data codewords.
    $maxBits = $dataCodewords * 8;
    for ($i = 0; $i < 4 && count($bits) < $maxBits; $i++) $bits[] = 0;
    while (count($bits) % 8 !== 0) $bits[] = 0;
    $bytes = [];
    for ($i = 0; $i < count($bits); $i += 8) {
        $v = 0;
        for ($j = 0; $j < 8; $j++) $v = ($v << 1) | (int)$bits[$i + $j];
        $bytes[] = $v;
    }
    $pads = [0xec, 0x11];
    $k = 0;
    while (count($bytes) < $dataCodewords) $bytes[] = $pads[($k++) % 2];
    return array_slice($bytes, 0, $dataCodewords);
}

function botella_qr_place_finder(array &$m, array &$res, int $x, int $y): void {
    for ($dy = -1; $dy <= 7; $dy++) {
        for ($dx = -1; $dx <= 7; $dx++) {
            $xx = $x + $dx; $yy = $y + $dy;
            if ($xx < 0 || $yy < 0 || $yy >= count($m) || $xx >= count($m)) continue;
            $dark = ($dx >= 0 && $dx <= 6 && $dy >= 0 && $dy <= 6 && ($dx == 0 || $dx == 6 || $dy == 0 || $dy == 6 || ($dx >= 2 && $dx <= 4 && $dy >= 2 && $dy <= 4)));
            $m[$yy][$xx] = $dark ? 1 : 0;
            $res[$yy][$xx] = true;
        }
    }
}


function botella_qr_format_bits(int $mask): int {
    /*
     * Bits de formato QR para nivel de corrección L y máscara indicada.
     * Evita error 500 por función faltante al renderizar etiqueta QR.
     */
    $mask = $mask & 7;
    $eccLevelL = 1; // QR format ECL: M=0, L=1, H=2, Q=3
    $data = ($eccLevelL << 3) | $mask;
    $rem = $data << 10;
    $generator = 0x537;
    for ($i = 14; $i >= 10; $i--) {
        if ((($rem >> $i) & 1) !== 0) {
            $rem ^= ($generator << ($i - 10));
        }
    }
    return ((($data << 10) | ($rem & 0x3ff)) ^ 0x5412) & 0x7fff;
}

function botella_qr_svg(string $text, int $modulesPx = 148): string {
    /*
     * QR local, sin librerías externas ni internet.
     * Versión 5-L, modo byte, suficiente para el label_code actual (VARCHAR 80).
     * El dato escaneado sigue siendo exactamente production_labels.label_code.
     */
    $text = trim($text);
    if ($text === '') $text = 'SIN-CODIGO';
    $bytes = array_values(unpack('C*', $text));
    if (count($bytes) > 100) $bytes = array_slice($bytes, 0, 100);

    $version = 5;
    $size = 17 + 4 * $version; // 37
    $dataCodewords = 108;
    $eccCodewords = 26;
    $mask = 0;

    $bits = [];
    foreach ([0,1,0,0] as $b) $bits[] = $b; // byte mode
    $len = count($bytes);
    for ($i = 7; $i >= 0; $i--) $bits[] = ($len >> $i) & 1;
    foreach ($bytes as $b) for ($i = 7; $i >= 0; $i--) $bits[] = ($b >> $i) & 1;

    $data = botella_qr_bits_to_codewords($bits, $dataCodewords);
    $ecc = botella_qr_rs_ecc($data, $eccCodewords);
    $codewords = array_merge($data, $ecc);
    $dataBits = [];
    foreach ($codewords as $cw) for ($i = 7; $i >= 0; $i--) $dataBits[] = ($cw >> $i) & 1;

    $m = array_fill(0, $size, array_fill(0, $size, 0));
    $res = array_fill(0, $size, array_fill(0, $size, false));

    botella_qr_place_finder($m, $res, 0, 0);
    botella_qr_place_finder($m, $res, $size - 7, 0);
    botella_qr_place_finder($m, $res, 0, $size - 7);

    for ($i = 8; $i < $size - 8; $i++) {
        $v = ($i % 2 === 0) ? 1 : 0;
        $m[6][$i] = $v; $res[6][$i] = true;
        $m[$i][6] = $v; $res[$i][6] = true;
    }

    // Alignment patterns versión 5: centros 6 y 30; solo se coloca el central inferior/derecho que no invade finder.
    foreach ([[30,30]] as $c) {
        [$cx,$cy] = $c;
        for ($dy=-2;$dy<=2;$dy++) for ($dx=-2;$dx<=2;$dx++) {
            $xx=$cx+$dx; $yy=$cy+$dy;
            $dark = max(abs($dx), abs($dy)) !== 1;
            $m[$yy][$xx] = $dark ? 1 : 0; $res[$yy][$xx] = true;
        }
    }

    // Áreas de formato reservadas.
    for ($i=0;$i<9;$i++) { if ($i!=6) { $res[8][$i]=true; $res[$i][8]=true; } }
    for ($i=0;$i<8;$i++) { $res[8][$size-1-$i]=true; $res[$size-1-$i][8]=true; }
    $m[$size-8][8] = 1; $res[$size-8][8] = true; // dark module

    $bitIndex = 0;
    $dir = -1;
    for ($x = $size - 1; $x > 0; $x -= 2) {
        if ($x == 6) $x--;
        for ($yy = 0; $yy < $size; $yy++) {
            $y = ($dir === -1) ? ($size - 1 - $yy) : $yy;
            for ($xx = $x; $xx >= $x - 1; $xx--) {
                if ($res[$y][$xx]) continue;
                $bit = $dataBits[$bitIndex++] ?? 0;
                // Mask 0: (row + col) % 2 == 0
                if ((($y + $xx) % 2) === 0) $bit ^= 1;
                $m[$y][$xx] = $bit;
            }
        }
        $dir *= -1;
    }

    $fmt = botella_qr_format_bits($mask);
    // Formato alrededor de finder superior izquierdo.
    // Nota: este orden queda validado contra lectores QR reales / OpenCV.
    for ($i=0;$i<=5;$i++) $m[$i][8] = ($fmt >> $i) & 1;
    $m[7][8] = ($fmt >> 6) & 1;
    $m[8][8] = ($fmt >> 7) & 1;
    $m[8][7] = ($fmt >> 8) & 1;
    for ($i=9;$i<15;$i++) $m[8][14-$i] = ($fmt >> $i) & 1;
    // Copias de formato.
    for ($i=0;$i<8;$i++) $m[8][$size-1-$i] = ($fmt >> $i) & 1;
    for ($i=8;$i<15;$i++) $m[$size-15+$i][8] = ($fmt >> $i) & 1;
    $m[$size-8][8] = 1;

    $quiet = 4;
    $view = $size + ($quiet * 2);
    $rects = [];
    for ($y=0;$y<$size;$y++) {
        for ($x=0;$x<$size;$x++) {
            if ($m[$y][$x]) $rects[] = '<rect x="' . ($x + $quiet) . '" y="' . ($y + $quiet) . '" width="1" height="1"/>';
        }
    }
    return '<svg class="qr-svg" viewBox="0 0 ' . $view . ' ' . $view . '" width="' . (int)$modulesPx . '" height="' . (int)$modulesPx . '" shape-rendering="crispEdges" role="img" aria-label="QR ' . h($text) . '"><rect width="' . $view . '" height="' . $view . '" fill="#fff"/><g fill="#000">' . implode('', $rects) . '</g></svg>';
}





function botella_render_shift_report(PDO $pdo, int $companyId, int $locationId, string $workDate, int $shift, array $scopeNames): void {
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDate)) $workDate = botella_operational_today();
    if (!in_array($shift, [1,2,3], true)) $shift = botella_shift_now();

    $st = $pdo->prepare("
        SELECT
            cg.production_line_no,
            COALESCE(l.line_name, CONCAT('Línea ', cg.production_line_no)) AS line_name,
            p.sku,
            p.name AS product_name,
            COALESCE(w.code,'') AS warehouse_code,
            COALESCE(w.name,'') AS warehouse_name,
            cg.source_type,
            COUNT(*) AS registros,
            COALESCE(SUM(cg.pallets_qty),0) AS tarimas,
            COALESCE(SUM(cg.packages_qty),0) AS paquetes,
            COALESCE(SUM(cg.qty_total),0) AS piezas
        FROM production_capture_groups cg
        JOIN products p ON p.id = cg.product_id
        LEFT JOIN warehouses w ON w.id = COALESCE(cg.warehouse_id, cg.cf_wh_id)
        LEFT JOIN production_lines l ON l.company_id=cg.company_id AND l.location_id=cg.location_id AND l.line_no=cg.production_line_no
        WHERE cg.company_id=?
          AND cg.location_id=?
          AND cg.work_date=?
          AND cg.shift=?
        GROUP BY cg.production_line_no, line_name, p.sku, p.name, warehouse_code, warehouse_name, cg.source_type
        ORDER BY cg.production_line_no, p.sku, cg.source_type
    ");
    $st->execute([$companyId, $locationId, $workDate, $shift]);
    $rows = $st->fetchAll(PDO::FETCH_ASSOC);

    $totalTarimas = 0.0;
    $totalPaquetes = 0.0;
    $totalPiezas = 0.0;
    foreach ($rows as $r) {
        $totalTarimas += (float)($r['tarimas'] ?? 0);
        $totalPaquetes += (float)($r['paquetes'] ?? 0);
        $totalPiezas += (float)($r['piezas'] ?? 0);
    }

    [$companyName, $locationName] = $scopeNames;
    ?><!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Reporte Producción Botella · <?=h($workDate)?> · T<?=h((string)$shift)?></title>
<style>
@page{size:letter;margin:10mm}
body{font-family:Arial,Helvetica,sans-serif;color:#07152f;margin:0;background:#fff}
.header{border:2px solid #0f3b7a;border-radius:12px;padding:14px;margin-bottom:12px}
.brand{font-size:24px;font-weight:900;letter-spacing:.03em}
.sub{color:#475569;font-size:13px;margin-top:4px}
.kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:12px 0}
.kpi{border:1px solid #cbd5e1;border-radius:10px;padding:10px}
.kpi .label{font-size:11px;text-transform:uppercase;color:#64748b;font-weight:800}
.kpi .value{font-size:24px;font-weight:900;margin-top:4px}
table{width:100%;border-collapse:collapse;font-size:12px}
th{background:#0f3b7a;color:#fff;text-align:left;padding:8px}
td{border-bottom:1px solid #dbe5f1;padding:7px;vertical-align:top}
.num{text-align:right;font-weight:800}
.footer{margin-top:14px;color:#64748b;font-size:11px}
.actions{margin-bottom:10px}
.actions button{font-weight:800;padding:6px 10px}
@media print{.actions{display:none}}
.tag-card{display:block;border:1px solid #dbe5f1;border-radius:16px;background:#fff;padding:12px;margin-bottom:8px;cursor:pointer}.tag-card.selected{border-color:#2563eb;background:#eff6ff;box-shadow:0 0 0 3px rgba(37,99,235,.10)}.tag-empty{border:1px dashed #f59e0b;background:#fffbeb;color:#92400e;border-radius:16px;padding:12px}.start-mode-card{display:block;border:1px solid #dbe5f1;border-radius:16px;background:#fff;padding:12px;cursor:pointer}.start-mode-card.active{border-color:#2563eb;background:#eff6ff;box-shadow:0 0 0 3px rgba(37,99,235,.10)}</style>
</head>
<body>
<div class="actions"><button onclick="window.print()">Imprimir reporte</button> <button onclick="window.close()">Cerrar</button></div>
<div class="header">
  <div class="brand">AGUA BLANQUITA · REPORTE DE PRODUCCIÓN BOTELLA</div>
  <div class="sub"><?=h($companyName)?> · <?=h($locationName)?> · Fecha operativa <?=h($workDate)?> · Turno T<?=h((string)$shift)?> · Generado <?=h(botella_now())?></div>
</div>
<div class="kpis">
  <div class="kpi"><div class="label">Tarimas</div><div class="value"><?=number_format($totalTarimas,0)?></div></div>
  <div class="kpi"><div class="label">Paquetes</div><div class="value"><?=number_format($totalPaquetes,0)?></div></div>
  <div class="kpi"><div class="label">Piezas</div><div class="value"><?=number_format($totalPiezas,0)?></div></div>
</div>
<table>
<thead><tr><th>Línea</th><th>Artículo</th><th>Almacén</th><th>Origen</th><th class="num">Reg.</th><th class="num">Tarimas</th><th class="num">Paquetes</th><th class="num">Piezas</th></tr></thead>
<tbody>
<?php if (!$rows): ?>
<tr><td colspan="8" style="text-align:center;color:#64748b">Sin producción registrada para este turno.</td></tr>
<?php else: foreach ($rows as $r): ?>
<tr>
  <td><?=h((string)$r['line_name'])?></td>
  <td><b><?=h((string)$r['sku'])?></b><br><?=h((string)$r['product_name'])?></td>
  <td><?=h((string)$r['warehouse_code'])?><br><span style="color:#64748b"><?=h((string)$r['warehouse_name'])?></span></td>
  <td><?=h((string)$r['source_type'])?></td>
  <td class="num"><?=number_format((float)$r['registros'],0)?></td>
  <td class="num"><?=number_format((float)$r['tarimas'],0)?></td>
  <td class="num"><?=number_format((float)$r['paquetes'],0)?></td>
  <td class="num"><?=number_format((float)$r['piezas'],0)?></td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
<div class="footer">BReport · Producción Botella · Fuente oficial inventory_moves / production_capture_groups.</div>
<script>window.addEventListener('load',()=>setTimeout(()=>window.print(),500));</script>
</body>
</html><?php
    exit;
}

if (isset($_GET['report']) && $_GET['report'] === 'shift') {
    $workDateReport = (string)($_GET['work_date'] ?? botella_operational_today());
    $shiftReport = (int)($_GET['shift'] ?? botella_shift_now());
    botella_render_shift_report($pdo, $companyId, $locationId, $workDateReport, $shiftReport, botella_scope_names($pdo, $companyId, $locationId));
}

if (isset($_GET['label_id'])) {
    $labelId = (int)$_GET['label_id'];
    /*
     * Prueba de impresión directa por navegador:
     * - autoprint=1: dispara window.print() al cargar la etiqueta.
     * - autoclose=1: intenta cerrar la ventana después de imprimir.
     * Nota: por seguridad del navegador, puede aparecer vista previa/diálogo.
     */
    $autoPrint = isset($_GET['autoprint']) || isset($_GET['silent']) || isset($_GET['print']);
    $autoClose = isset($_GET['autoclose']) || isset($_GET['close']) || isset($_GET['silent']);
    $hideBar = $autoPrint || isset($_GET['silent']);

    $st = $pdo->prepare("SELECT pl.*, p.sku, p.name AS product_name, po.packaging_name, po.pieces_per_package, po.packages_per_pallet, l.line_name
        FROM production_labels pl
        JOIN products p ON p.id=pl.product_id
        LEFT JOIN product_packaging_options po ON po.id=pl.packaging_option_id
        LEFT JOIN production_lines l ON l.id=pl.production_line_id
        WHERE pl.id=? AND pl.company_id=? AND pl.location_id=? LIMIT 1");
    $st->execute([$labelId, $companyId, $locationId]);
    $label = $st->fetch(PDO::FETCH_ASSOC);
    if (!$label) { http_response_code(404); exit('Etiqueta no encontrada'); }
    $pdo->prepare("UPDATE production_labels SET printed_at=NOW(), printed_by=? WHERE id=?")->execute([(int)$u['id'], $labelId]);

    $createdAt = (string)($label['created_at'] ?? botella_now());
    try {
        $dtLabel = new DateTimeImmutable($createdAt, new DateTimeZone(BREPORT_TZ));
    } catch (Throwable $e) {
        $dtLabel = new DateTimeImmutable('now', new DateTimeZone(BREPORT_TZ));
    }
    $fechaLabel = $dtLabel->format('d/m/Y');
    $horaLabel = $dtLabel->format('H:i:s');
    $turnoLabel = 'T' . (int)($label['shift'] ?? 0);
    $lineaLabel = (string)($label['line_name'] ?: ('Línea ' . (int)($label['production_line_no'] ?? 0)));
    $labelCode = (string)($label['label_code'] ?? '');
    $qrSvg = botella_qr_svg($labelCode, 235);
    ?>
<!doctype html><html lang="es"><head><meta charset="utf-8"><title>Etiqueta <?=h($labelCode)?></title>
<style>
@page{size:6in 4in landscape;margin:0}
*{box-sizing:border-box}
html,body{width:6in;height:4in;margin:0;background:#fff;color:#07152f;font-family:Arial,Helvetica,sans-serif;overflow:hidden}
.printbar{position:fixed;left:8px;top:8px;z-index:20;display:flex;gap:6px}.printbar button{border:1px solid #111;background:#fff;border-radius:4px;padding:4px 8px;font-weight:800}.printbar.hide{display:none}.print-status{display:none!important}
.sheet{width:6in;height:4in;padding:.13in;display:flex;align-items:center;justify-content:center}.label{width:5.74in;height:3.72in;border:2px solid #111;padding:.12in .16in;display:grid;grid-template-rows:.58in 1fr;gap:.09in;position:relative;overflow:hidden;background:#fff}
.product{text-align:center;font-size:24pt;font-weight:950;line-height:.96;text-transform:uppercase;border-bottom:2px solid #111;padding:.02in .05in .055in;display:flex;align-items:center;justify-content:center;white-space:normal;overflow:hidden;text-wrap:balance}.label-body{display:grid;grid-template-columns:2.18in 1fr;gap:.14in;min-height:0;align-items:stretch}.qr-panel{border:0;display:flex;align-items:center;justify-content:center;padding:0;background:#fff;min-width:0}.qr-svg{width:2.08in;height:2.08in;display:block}.data-panel{display:grid;grid-template-rows:.82in 1.16in .7in;gap:.08in;min-width:0}.info{display:grid;grid-template-columns:1fr 1fr;gap:.08in}.box{border:1.6px solid #111;padding:.065in .075in;display:flex;flex-direction:column;justify-content:center;min-width:0}.box .cap{font-size:7.3pt;font-weight:900;text-transform:uppercase;color:#26364f;letter-spacing:.01em}.box .val{font-size:12.5pt;font-weight:950;line-height:1.02;margin-top:.02in;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.box .val.small{font-size:11.5pt}.totalPieces{border:2px solid #111;padding:.08in .11in;display:flex;flex-direction:column;justify-content:center}.totalPieces .cap{font-size:10pt;font-weight:950;text-transform:uppercase}.totalPieces .val{font-size:42pt;font-weight:950;line-height:.88;letter-spacing:-.035em}.bottomTotals{display:grid;grid-template-columns:1fr 1fr;gap:.08in}.miniBox{border:1.6px solid #111;padding:.055in .075in;display:flex;flex-direction:column;justify-content:center}.miniBox .cap{font-size:8.2pt;font-weight:950;text-transform:uppercase;color:#26364f}.miniBox .val{font-size:22pt;font-weight:950;line-height:.9}.dateTimeVal{display:grid;gap:.02in}.dateTimeVal b{font-size:12.5pt;line-height:1.02}.dateTimeVal span{font-size:11pt;font-weight:900;line-height:1.02}.lineText{font-size:10.5pt!important}@media print{.printbar,.print-status{display:none!important}body{margin:0}.sheet{padding:.13in}.label{break-inside:avoid;page-break-inside:avoid}}
</style></head><body>
<div class="printbar <?= $hideBar ? 'hide' : '' ?>"><button onclick="window.print()">Imprimir</button><button onclick="window.close()">Cerrar</button></div>
<div id="printStatus" class="print-status"></div>
<div class="sheet">
  <section class="label">
    <div class="product"><?=h($label['product_name'])?></div>
    <div class="label-body">
      <div class="qr-panel">
        <?=$qrSvg?>
      </div>
      <div class="data-panel">
        <div class="info">
          <div class="box">
            <div class="cap">Fecha / Hora</div>
            <div class="dateTimeVal"><b><?=h($fechaLabel)?></b><span><?=h($horaLabel)?></span></div>
          </div>
          <div class="box">
            <div class="cap">Turno / Línea</div>
            <div class="val"><?=h($turnoLabel)?></div>
            <div class="val small lineText"><?=h($lineaLabel)?></div>
          </div>
        </div>
        <div class="totalPieces">
          <div class="cap">Total piezas</div>
          <div class="val"><?=number_format((float)$label['total_pieces'],0)?></div>
        </div>
        <div class="bottomTotals">
          <div class="miniBox"><div class="cap">Tarima</div><div class="val"><?=number_format((float)$label['pallets_qty'],0)?></div></div>
          <div class="miniBox"><div class="cap">Paquetes</div><div class="val"><?=number_format((float)$label['packages_qty'],0)?></div></div>
        </div>
      </div>
    </div>
  </section>
</div>
<script>
const LABEL_AUTO_PRINT = <?= $autoPrint ? 'true' : 'false' ?>;
const LABEL_AUTO_CLOSE = <?= $autoClose ? 'true' : 'false' ?>;
let labelCloseRequested = false;
function updatePrintStatus(msg){
  // Estado oculto: no se muestra sobre la etiqueta ni en la vista previa.
  const el = document.getElementById('printStatus');
  if(el) el.dataset.status = msg;
}
function requestLabelWindowClose(){
  if(!LABEL_AUTO_CLOSE || labelCloseRequested) return;
  labelCloseRequested = true;
  updatePrintStatus('Impresión enviada. Cerrando ventana...');
  setTimeout(()=>{
    try{ window.close(); }catch(e){}
    setTimeout(()=>{
      if(!window.closed){
        updatePrintStatus('La etiqueta fue enviada. Si el navegador no cerró esta ventana, ciérrala manualmente.');
      }
    },700);
  },350);
}
window.addEventListener('load',()=>{
  if(LABEL_AUTO_PRINT){
    updatePrintStatus('Preparando impresión automática de etiqueta...');
    setTimeout(()=>{
      try{ window.focus(); }catch(e){}
      try{ window.print(); }catch(e){ updatePrintStatus('No se pudo abrir impresión automáticamente. Usa Imprimir.'); }
    },750);
    // Respaldo: algunos navegadores no disparan afterprint si el usuario cancela o si la impresora responde lento.
    setTimeout(()=>{
      if(LABEL_AUTO_CLOSE && !labelCloseRequested){
        updatePrintStatus('Si ya imprimiste, puedes cerrar esta ventana.');
      }
    },18000);
    // Evita ventanas de etiqueta abiertas indefinidamente cuando el navegador no dispara afterprint.
    setTimeout(()=>{
      if(LABEL_AUTO_CLOSE && !labelCloseRequested){
        requestLabelWindowClose();
      }
    },120000);
  }
});
window.addEventListener('afterprint', requestLabelWindowClose);
</script>
</body></html><?php
    exit;
}

[$scopeCompanyName, $scopeLocationName] = botella_scope_names($pdo, $companyId, $locationId);
$csrf = csrf_token();
$today = botella_operational_today();
$currentShift = botella_shift_now();
$canAdmin = botella_is_admin();
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BReport · Producción Botella</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root{--bg:#eef3f8;--card:#fff;--ink:#102033;--muted:#64748b;--line:#d9e2ee;--blue:#2563eb;--green:#16a34a;--orange:#f59e0b;--red:#dc2626;}
body{background:var(--bg);color:var(--ink);font-family:Inter,system-ui,-apple-system,"Segoe UI",sans-serif}.app-header{background:linear-gradient(135deg,#0f172a,#1d4ed8);color:#fff;border-radius:0 0 28px 28px;padding:22px 0 34px}.shell{max-width:1440px;margin:0 auto;padding:0 18px}.cardx{background:#fff;border:1px solid var(--line);border-radius:22px;box-shadow:0 16px 42px rgba(15,23,42,.08)}.kpi{padding:18px}.kpi .num{font-size:34px;font-weight:900}.muted{color:var(--muted)}.btn-pill{border-radius:999px}.line-card{cursor:pointer;transition:.15s}.line-card:hover{transform:translateY(-2px)}.status-dot{width:10px;height:10px;border-radius:50%;display:inline-block}.dot-run{background:var(--green)}.dot-off{background:#94a3b8}.floating-plc{position:fixed;right:22px;bottom:22px;z-index:20;border-radius:999px;padding:14px 18px;box-shadow:0 18px 45px rgba(37,99,235,.35)}.table td,.table th{vertical-align:middle}.form-control,.form-select{background:#fff!important;color:#111!important}.badge-soft{background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe}.modal-content{border-radius:22px;border:0}.modal-header{background:#0f172a;color:#fff;border-radius:22px 22px 0 0}.modal-header .btn-close{filter:invert(1)}
.line-card{border:1px solid var(--line);position:relative;overflow:hidden}.line-card:before{content:"";position:absolute;left:0;top:0;bottom:0;width:6px;background:#94a3b8}.line-card.waiting{background:linear-gradient(135deg,#fff7ed,#fff);border-color:#fed7aa}.line-card.waiting:before{background:var(--orange)}.line-card.running{background:linear-gradient(135deg,#ecfdf5,#fff);border-color:#86efac}.line-card.running:before{background:var(--green)}.line-card .line-action-hint{font-size:.78rem;color:#64748b}.badge-waiting{background:#fef3c7;color:#92400e;border:1px solid #fcd34d}.badge-running{background:#dcfce7;color:#166534;border:1px solid #86efac}.system-popup .modal-header{background:linear-gradient(135deg,#0f172a,#1d4ed8)}.system-icon{width:44px;height:44px;border-radius:16px;display:inline-flex;align-items:center;justify-content:center;background:#eff6ff;color:#1d4ed8;font-size:1.35rem}.system-popup.success .system-icon{background:#dcfce7;color:#166534}.system-popup.warning .system-icon{background:#fef3c7;color:#92400e}.system-popup.danger .system-icon{background:#fee2e2;color:#991b1b}.system-popup .modal-body{font-size:1rem}.app-toast-wrap{position:fixed;right:18px;top:18px;z-index:3000;display:grid;gap:10px;max-width:min(420px,calc(100vw - 32px))}.app-toast{background:#fff;border:1px solid #dbe2ee;border-left:6px solid #2563eb;border-radius:18px;box-shadow:0 18px 45px rgba(15,23,42,.18);padding:12px 14px}.app-toast.success{border-left-color:#16a34a}.app-toast.warning{border-left-color:#f59e0b}.app-toast.danger{border-left-color:#dc2626}
/* Modales de sistema siempre por encima de modales operativos */
.modal.system-popup{z-index:2105!important}.modal-backdrop.system-modal-backdrop{z-index:2100!important}.modal.system-popup .modal-dialog{position:relative;z-index:2106}.modal.system-popup .modal-content{box-shadow:0 30px 90px rgba(15,23,42,.38)}
.start-mode-card{border:1px solid #dbeafe;background:#f8fbff;border-radius:18px;padding:12px;cursor:pointer;transition:.15s}.start-mode-card.active{border-color:#2563eb;background:linear-gradient(135deg,#eff6ff,#fff);box-shadow:0 0 0 4px rgba(37,99,235,.12)}.tag-card{border:1px solid #e2e8f0;border-radius:16px;padding:10px;background:#fff}.tag-card.selected{border-color:#2563eb;background:#eff6ff}.tag-list{display:grid;gap:8px}.tag-empty{border:1px dashed #f59e0b;background:#fffbeb;color:#92400e;border-radius:16px;padding:12px}
</style>
</head>
<body>
<header class="app-header">
  <div class="shell d-flex justify-content-between align-items-start gap-3 flex-wrap">
    <div>
      <div class="small opacity-75">BReport · MES multiempresa</div>
      <h1 class="h3 fw-black mb-1">Producción Botella</h1>
      <div class="opacity-75"><?=h($scopeCompanyName)?> · <?=h($scopeLocationName)?> · Zona <?=h(BREPORT_TZ)?></div>
    </div>
    <div class="d-flex gap-2 flex-wrap">
      <a href="dashboard.php" class="btn btn-light btn-pill"><i class="bi bi-grid me-1"></i>Dashboard</a>
      <?php if ($canAdmin): ?><button class="btn btn-success btn-pill" data-bs-toggle="modal" data-bs-target="#modalTarget"><i class="bi bi-bullseye me-1"></i>Objetivo día</button><?php endif; ?>
      <button class="btn btn-light btn-pill" data-bs-toggle="modal" data-bs-target="#modalConfig" onclick="openConfig()"><i class="bi bi-gear me-1"></i>Configuración</button>
    </div>
  </div>
</header>

<main class="shell" style="margin-top:-22px">
  <section class="row g-3 mb-3">
    <div class="col-md-3"><div class="cardx kpi"><div class="muted">Fecha operativa</div><div class="num fs-2" id="kDate"><?=h($today)?></div></div></div>
    <div class="col-md-3"><div class="cardx kpi"><div class="muted">Turno actual</div><div class="num" id="kShift">T<?=$currentShift?></div></div></div>
    <div class="col-md-3"><div class="cardx kpi"><div class="muted">Producido día</div><div class="num" id="kProduced">0</div><div class="muted">piezas</div></div></div>
    <div class="col-md-3"><div class="cardx kpi"><div class="muted">Avance objetivo</div><div class="num" id="kProgress">0%</div><div class="progress" style="height:8px"><div class="progress-bar" id="barProgress" style="width:0%"></div></div></div></div>
  </section>

  <section class="row g-3">
    <div class="col-lg-4">
      <div class="cardx p-3 h-100">
        <div class="d-flex justify-content-between align-items-center mb-3"><h2 class="h5 m-0">Líneas de producción</h2><button class="btn btn-sm btn-outline-primary" onclick="loadAll()"><i class="bi bi-arrow-clockwise"></i></button></div>
        <div id="linesBox" class="vstack gap-2"></div>
      </div>
    </div>
    <div class="col-lg-8">
      <div class="cardx p-3">
        <div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2"><h2 class="h5 m-0">Captura manual de producción · 1 tarima</h2><span class="badge badge-soft">Genera IN real en inventory_moves</span></div>
        <form id="formCapture" class="row g-3">
          <div class="col-md-4"><label class="form-label">Sesión activa</label><select class="form-select" id="capSession" required></select></div>
          <div class="col-md-4"><label class="form-label">Artículo</label><select class="form-select" id="capProduct" required></select></div>
          <div class="col-md-4"><label class="form-label">Almacén destino</label><select class="form-select" id="capWarehouse" required disabled></select><div class="form-text">Automático: ALM01</div></div>
          <div class="col-md-4"><label class="form-label">Embalaje</label><select class="form-select" id="capPackaging"></select></div>
          <input type="hidden" id="capPallets" value="1">
          <input type="hidden" id="capLoosePackages" value="0">
          <input type="hidden" id="capLoosePieces" value="0">
          <div class="col-12">
            <div class="alert alert-primary py-2 mb-0">
              <b>Producción normal:</b> cada reporte registra <b>1 tarima completa</b>. Los paquetes o botellas sueltas se capturan únicamente al finalizar turno como <b>Última tarima</b>.
            </div>
          </div>
          <div class="col-12"><label class="form-label">Observación</label><input class="form-control" id="capNotes" placeholder="Opcional"></div>
          <div class="col-12 d-flex justify-content-between align-items-center flex-wrap gap-2">
            <div class="fw-bold" id="calcPreview">Selecciona artículo y empaque para calcular.</div>
            <button class="btn btn-primary btn-pill"><i class="bi bi-box-seam me-1"></i>Reportar producción y generar etiqueta</button>
          </div>
        </form>
      </div>
      <div class="cardx p-3 mt-3">
        <div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
          <h2 class="h5 m-0">Última etiqueta impresa</h2>
          <div class="d-flex gap-2">
            <button class="btn btn-sm btn-outline-primary" onclick="loadLastCapture()" title="Actualizar última etiqueta"><i class="bi bi-arrow-clockwise"></i></button>
            <button class="btn btn-sm btn-primary" onclick="openCaptureHistory()"><i class="bi bi-clock-history me-1"></i>Historial de tarimas</button>
          </div>
        </div>
        <div id="lastLabelBox" class="border rounded-4 p-3 bg-light text-muted">
          Sin etiqueta impresa en el día seleccionado.
        </div>
      </div>
    </div>
  </section>
</main>

<button class="btn btn-primary floating-plc" id="btnPlcFloating" type="button" onclick="handlePlcFloatingClick()"><i class="bi bi-cpu me-1"></i>PLC <span class="badge bg-light text-primary" id="plcCount">0</span></button>

<div class="modal fade" id="modalStart" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Iniciar turno por línea</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="formStart"><div class="modal-body row g-3"><div class="col-md-6"><label class="form-label">Fecha operativa</label><input type="date" class="form-control" id="startDate" value="<?=h($today)?>"></div><div class="col-md-6"><label class="form-label">Turno</label><select class="form-select" id="startShift"><option value="1">Turno 1</option><option value="2">Turno 2</option><option value="3">Turno 3</option></select></div><div class="col-12"><label class="form-label">Línea</label><select class="form-select" id="startLine"></select></div><div class="col-12"><label class="form-label fw-bold">Tipo de reportaje</label><div class="row g-2"><div class="col-md-6"><label class="start-mode-card w-100 active" id="modeManualCard"><input class="form-check-input me-2" type="radio" name="startReportMode" id="startModeManual" value="MANUAL" checked> <b>Manual</b><div class="small muted mt-1">El operador reporta cada tarima desde pantalla.</div></label></div><div class="col-md-6"><label class="start-mode-card w-100" id="modeAutoCard"><input class="form-check-input me-2" type="radio" name="startReportMode" id="startModeAuto" value="AUTOMATIC"> <b>Automático PLC</b><div class="small muted mt-1">La línea queda escuchando la señal del TAG configurado.</div></label></div></div></div><div class="col-12 d-none" id="startPlcBox"><label class="form-label fw-bold">TAG PLC para esta línea</label><select class="form-select" id="startPlcTag"><option value="">Buscando TAG...</option></select><div class="tag-list mt-2" id="startPlcTagList"></div><div class="small muted mt-2">Si hay más de un TAG para la línea, selecciona cuál va a contar producción en esta sesión.</div></div><div class="col-12"><div class="alert alert-info mb-0">Puedes operar las 3 líneas al mismo tiempo, pero una misma línea no puede abrirse dos veces hasta finalizar su turno activo.</div></div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Cancelar</button><button class="btn btn-primary">Iniciar sesión</button></div></form></div></div></div>


<div class="modal fade system-popup" id="modalSystemNotify" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="systemNotifyTitle">Aviso del sistema</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="d-flex gap-3 align-items-start"><div class="system-icon" id="systemNotifyIcon"><i class="bi bi-info-circle"></i></div><div class="flex-fill"><div id="systemNotifyMessage"></div></div></div></div><div class="modal-footer"><button class="btn btn-primary btn-pill" data-bs-dismiss="modal" type="button">Entendido</button></div></div></div></div>

<div class="modal fade system-popup" id="modalSystemConfirm" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="systemConfirmTitle">Confirmar acción</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="d-flex gap-3 align-items-start"><div class="system-icon"><i class="bi bi-question-circle"></i></div><div class="flex-fill" id="systemConfirmMessage"></div></div></div><div class="modal-footer"><button class="btn btn-outline-secondary btn-pill" id="systemConfirmCancel" type="button">Cancelar</button><button class="btn btn-primary btn-pill" id="systemConfirmOk" type="button">Confirmar</button></div></div></div></div>

<div class="modal fade system-popup" id="modalSystemPrompt" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="systemPromptTitle">Capturar nota</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><label class="form-label fw-bold" id="systemPromptMessage">Nota</label><textarea class="form-control" id="systemPromptInput" rows="3"></textarea></div><div class="modal-footer"><button class="btn btn-outline-secondary btn-pill" id="systemPromptCancel" type="button">Cancelar</button><button class="btn btn-primary btn-pill" id="systemPromptOk" type="button">Aceptar</button></div></div></div></div>
<div class="app-toast-wrap" id="appToastWrap"></div>

<div class="modal fade" id="modalEnd" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Finalizar turno activo · Última tarima</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="formEnd"><div class="modal-body row g-3"><input type="hidden" id="endSession"><div class="col-12"><div class="alert alert-warning"><b>Última tarima:</b> se toma el <b>mismo artículo activo</b> que está seleccionado en la captura manual. El embalaje se usa para calcular piezas por paquete cuando exista; si es embalaje principal, el sistema usa la configuración del artículo. El operador solo captura paquetes sueltos y piezas sueltas; al finalizar se genera la entrada real, etiqueta y reporte total del turno.</div></div><div class="col-12"><div class="card border-primary bg-light"><div class="card-body py-3"><div class="small text-uppercase text-muted fw-bold mb-1">Artículo / embalaje activo para cierre</div><div class="fw-bold" id="endWorkInfo">Selecciona artículo antes de finalizar turno. El embalaje puede ser principal/configuración del artículo.</div><div class="small text-muted mt-1">Almacén automático: ALM01. No se permite cierre directo a rutas.</div></div></div></div><div class="col-md-4"><label class="form-label">Artículo</label><select class="form-select" id="endProduct" disabled></select></div><div class="col-md-4"><label class="form-label">Almacén destino</label><select class="form-select" id="endWarehouse" disabled></select></div><div class="col-md-4"><label class="form-label">Embalaje</label><select class="form-select" id="endPackaging" disabled></select></div><div class="col-md-4"><label class="form-label">Paquetes sueltos última tarima</label><input type="number" min="0" step="1" class="form-control" id="endLoosePackages" value="0"></div><div class="col-md-4"><label class="form-label">Piezas sueltas última tarima</label><input type="number" min="0" step="1" class="form-control" id="endLoosePieces" value="0"></div><div class="col-md-4"><label class="form-label">Notas cierre</label><input class="form-control" id="endNotes"></div><div class="col-12"><div class="alert alert-info py-2 mb-0" id="endCalcPreview">Última tarima sin paquetes ni piezas sueltas capturadas.</div></div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Cancelar</button><button class="btn btn-danger">Finalizar turno y generar reporte</button></div></form></div></div></div>


<div class="modal fade" id="modalCaptureHistory" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="bi bi-clock-history me-1"></i>Historial de tarimas producidas</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="row g-3 align-items-end mb-3"><div class="col-md-3"><label class="form-label">Desde</label><input type="date" class="form-control" id="historyFrom" value="<?=h($today)?>"></div><div class="col-md-3"><label class="form-label">Hasta</label><input type="date" class="form-control" id="historyTo" value="<?=h($today)?>"></div><div class="col-md-3"><button class="btn btn-primary w-100" type="button" onclick="loadCaptureHistory()"><i class="bi bi-search me-1"></i>Consultar</button></div><div class="col-md-3"><div class="alert alert-info py-2 mb-0" id="historyCount">Selecciona rango.</div></div></div><div class="table-responsive"><table class="table table-sm table-hover align-middle"><thead><tr><th>Fecha/Hora</th><th>Línea</th><th>Producto</th><th>Tarimas</th><th>Paquetes</th><th>Piezas</th><th>Origen</th><th>Almacén</th><th>Etiqueta</th></tr></thead><tbody id="historyBody"><tr><td colspan="9" class="text-center text-muted">Consulta un rango para ver el historial.</td></tr></tbody></table></div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Cerrar</button></div></div></div></div>

<div class="modal fade" id="modalTarget" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Objetivo de piezas del día</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="formTarget"><div class="modal-body row g-3"><div class="col-md-6"><label class="form-label">Fecha</label><input type="date" class="form-control" id="targetDate" value="<?=h($today)?>"></div><div class="col-md-6"><label class="form-label">Objetivo piezas</label><input type="number" min="0" step="1" class="form-control" id="targetPieces" value="0"></div><div class="col-12"><label class="form-label">Notas</label><input class="form-control" id="targetNotes"></div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Cancelar</button><button class="btn btn-success">Guardar objetivo</button></div></form></div></div></div>

<div class="modal fade" id="modalConfig" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="bi bi-gear me-1"></i>Configuración Producción Botella</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="formConfig"><div class="modal-body"><div class="alert alert-primary">El reporte automático PLC se activa manualmente. Deshabilitado, el módulo trabaja solo con captura manual y no consulta señales PLC.</div><div class="form-check form-switch mb-3"><input class="form-check-input" type="checkbox" role="switch" id="cfgPlcEnabled"><label class="form-check-label fw-bold" for="cfgPlcEnabled">Habilitar reportaje automático por PLC</label></div><label class="form-label">Frecuencia de consulta PLC, segundos</label><input type="number" min="5" max="120" step="1" class="form-control" id="cfgPollSeconds" value="10"><div class="small muted mt-2">Manual = no se lee <b>production_plc_tags</b>. Automático = lee señales activas por empresa/localidad y permite confirmarlas como producción.</div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Cancelar</button><?php if ($canAdmin): ?><button class="btn btn-primary">Guardar configuración</button><?php endif; ?></div></form></div></div></div>


<div class="modal fade" id="modalSwitchAuto" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="bi bi-cpu me-1"></i>Cambiar sesión manual a automático PLC</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="formSwitchAuto"><div class="modal-body row g-3"><div class="col-12"><div class="alert alert-warning mb-0"><b>Autorización requerida:</b> esta acción cambia la línea activa de captura manual a conteo automático por TAG PLC. El cambio queda aplicado a la sesión en turno.</div></div><div class="col-md-6"><label class="form-label fw-bold">Sesión / línea activa</label><select class="form-select" id="switchSession"></select><div class="small muted mt-1">Solo se muestran sesiones iniciadas en modo manual.</div></div><div class="col-md-6"><label class="form-label fw-bold">Código de autorización</label><input type="password" inputmode="numeric" autocomplete="off" class="form-control" id="switchAuthCode" placeholder="Código requerido"></div><div class="col-12"><label class="form-label fw-bold">TAG PLC configurado para la línea</label><select class="form-select" id="switchPlcTag"><option value="">Selecciona sesión para consultar TAG</option></select><div id="switchTagList" class="tag-list mt-2"></div></div><div class="col-12"><div class="small muted">Si existe más de un TAG activo para la línea, selecciona cuál recibirá la señal de conteo para esta sesión.</div></div></div><div class="modal-footer"><button class="btn btn-outline-secondary btn-pill" data-bs-dismiss="modal" type="button">Cancelar</button><button class="btn btn-primary btn-pill" type="submit"><i class="bi bi-check2-circle me-1"></i>Confirmar automático</button></div></form></div></div></div>


<div class="modal fade" id="modalSwitchManual" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="bi bi-hand-index-thumb me-1"></i>Cambiar sesión automática a manual</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="formSwitchManual"><div class="modal-body row g-3"><div class="col-12"><div class="alert alert-warning mb-0"><b>Autorización requerida:</b> esta acción detiene el conteo automático PLC para la sesión seleccionada y deja la línea en captura manual.</div></div><div class="col-md-6"><label class="form-label fw-bold">Sesión / línea en automático</label><select class="form-select" id="switchManualSession"></select><div class="small muted mt-1">Solo se muestran sesiones iniciadas en modo automático PLC.</div></div><div class="col-md-6"><label class="form-label fw-bold">Código de autorización</label><input type="password" inputmode="numeric" autocomplete="off" class="form-control" id="switchManualAuthCode" placeholder="Código requerido"></div><div class="col-12"><div id="switchManualInfo" class="tag-list mt-2"></div></div><div class="col-12"><div class="small muted">Código requerido para volver a manual: <b>2304</b>. Al cambiar, la sesión conserva su turno activo, pero ya no procesará señales PLC.</div></div></div><div class="modal-footer"><button class="btn btn-outline-secondary btn-pill" data-bs-dismiss="modal" type="button">Cancelar</button><button class="btn btn-primary btn-pill" type="button" onclick="bootstrap.Modal.getOrCreateInstance(document.getElementById('modalPlc')).show(); loadPlc(false);"><i class="bi bi-cpu me-1"></i>Ver señales</button><button class="btn btn-warning btn-pill" type="submit"><i class="bi bi-check2-circle me-1"></i>Confirmar manual</button></div></form></div></div></div>

<div class="modal fade" id="modalPlc" tabindex="-1"><div class="modal-dialog modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="bi bi-cpu me-1"></i>Señales PLC pendientes</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="alert alert-info">Las señales se leen desde <b>production_plc_tags</b> filtrando por empresa y localidad. En modo automático, BReport procesa la señal PLC sin clic manual usando el artículo y embalaje seleccionados en captura manual: genera IN real, etiqueta y deja la etiqueta pendiente para impresión por VB. Mantenimiento solo resuelve una señal que no debe contarse.</div><div class="table-responsive"><table class="table"><thead><tr><th>Tag</th><th>Línea</th><th>Artículo activo</th><th>Almacén</th><th>Estado</th><th>Acciones</th></tr></thead><tbody id="plcBody"></tbody></table></div></div><div class="modal-footer"><button class="btn btn-outline-warning btn-pill" onclick="openSwitchManualModal()" type="button"><i class="bi bi-hand-index-thumb me-1"></i>Cambiar a manual</button><button class="btn btn-outline-primary" onclick="loadPlc()" type="button">Actualizar</button></div></div></div></div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const CSRF = <?=json_encode($csrf)?>;
let state = {lines:[],products:[],warehouses:[],sessions:[],packaging:[],summary:null,signal_settings:{plc_enabled:0,report_mode:'MANUAL',poll_seconds:10}};
let botellaPlcAutoProcessing = false;
let botellaPlcAutoProcessed = new Set();
const API_ENDPOINT = window.location.pathname.split('/').pop() || 'produccion_botella.php';
const $ = id => document.getElementById(id);
async function api(action, opts={}){
  const res = await fetch(`${API_ENDPOINT}?action=${action}${opts.query||''}`, {method: opts.method||'GET', headers:{'Content-Type':'application/json','X-CSRF':CSRF}, body: opts.body?JSON.stringify(opts.body):undefined});
  const data = await res.json().catch(()=>({ok:false,error:'Respuesta inválida'}));
  if(!data.ok) throw new Error(data.error||'Error');
  return data;
}
function fmt(n){return Number(n||0).toLocaleString('es-MX',{maximumFractionDigits:0});}

function hText(v){return String(v??'').replace(/[&<>\"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));}
const h = hText;
const iconByType={success:'bi-check-circle',warning:'bi-exclamation-triangle',danger:'bi-x-octagon',info:'bi-info-circle'};
function elevateSystemModal(modalEl){
  if(!modalEl) return;
  modalEl.style.zIndex='2105';
  modalEl.addEventListener('shown.bs.modal',()=>{
    modalEl.style.zIndex='2105';
    const backs=[...document.querySelectorAll('.modal-backdrop')];
    const last=backs[backs.length-1];
    if(last){ last.classList.add('system-modal-backdrop'); last.style.zIndex='2100'; }
  },{once:true});
  modalEl.addEventListener('hidden.bs.modal',()=>{
    modalEl.style.zIndex='';
    document.querySelectorAll('.modal-backdrop.system-modal-backdrop').forEach(b=>b.remove());
    if(document.querySelector('.modal.show')) document.body.classList.add('modal-open');
  },{once:true});
}
function appToast(message,type='info',title='Aviso'){
  const wrap=$('appToastWrap'); if(!wrap){ console.log(message); return; }
  const el=document.createElement('div'); el.className='app-toast '+type;
  el.innerHTML=`<div class="fw-bold mb-1">${hText(title)}</div><div>${hText(message)}</div>`;
  wrap.appendChild(el);
  setTimeout(()=>{el.style.opacity='0';el.style.transform='translateY(-6px)';setTimeout(()=>el.remove(),250);},3800);
}
function appNotify(message,type='info',title='Aviso del sistema',autoClose=true){
  const modalEl=$('modalSystemNotify');
  if(!modalEl){ appToast(String(message||''),type,title); return; }
  modalEl.classList.remove('success','warning','danger','info'); modalEl.classList.add(type);
  $('systemNotifyTitle').textContent=title;
  $('systemNotifyIcon').innerHTML=`<i class="bi ${iconByType[type]||iconByType.info}"></i>`;
  $('systemNotifyMessage').innerHTML=String(message||'');
  const m=bootstrap.Modal.getOrCreateInstance(modalEl);
  elevateSystemModal(modalEl);
  m.show();
  if(autoClose && type!=='danger') setTimeout(()=>{try{m.hide();}catch(e){}},2300);
}
function appConfirm(message,title='Confirmar acción',okText='Confirmar'){
  return new Promise(resolve=>{
    const el=$('modalSystemConfirm'); if(!el){ resolve(false); return; }
    $('systemConfirmTitle').textContent=title;
    $('systemConfirmMessage').innerHTML=String(message||'');
    $('systemConfirmOk').textContent=okText;
    const m=bootstrap.Modal.getOrCreateInstance(el);
    let done=false;
    const finish=(val)=>{ if(done) return; done=true; $('systemConfirmOk').onclick=null; $('systemConfirmCancel').onclick=null; resolve(val); };
    $('systemConfirmOk').onclick=()=>{ finish(true); m.hide(); };
    $('systemConfirmCancel').onclick=()=>{ finish(false); m.hide(); };
    el.addEventListener('hidden.bs.modal', ()=>finish(false), {once:true});
    elevateSystemModal(el);
    m.show();
  });
}
function appPrompt(message,title='Capturar nota',defaultValue=''){
  return new Promise(resolve=>{
    const el=$('modalSystemPrompt'); if(!el){ resolve(defaultValue||''); return; }
    $('systemPromptTitle').textContent=title;
    $('systemPromptMessage').textContent=message;
    $('systemPromptInput').value=defaultValue||'';
    const m=bootstrap.Modal.getOrCreateInstance(el);
    let done=false;
    const finish=(val)=>{ if(done) return; done=true; $('systemPromptOk').onclick=null; $('systemPromptCancel').onclick=null; resolve(val); };
    $('systemPromptOk').onclick=()=>{ const val=$('systemPromptInput').value; finish(val); m.hide(); };
    $('systemPromptCancel').onclick=()=>{ finish(null); m.hide(); };
    el.addEventListener('hidden.bs.modal', ()=>finish(null), {once:true});
    elevateSystemModal(el);
    m.show(); setTimeout(()=>$('systemPromptInput')?.focus(),250);
  });
}
window.alert=(msg)=>appNotify(hText(String(msg||'')),'danger','Aviso del sistema',false);

function prepareLabelPrintWindow(){
  // Abrimos la ventana dentro del clic del usuario para reducir bloqueo de pop-ups.
  const name = 'botella_label_print_' + Date.now();
  const features = 'popup=yes,width=760,height=560,left=80,top=80';
  const w = window.open('about:blank', name, features);
  if(w){
    try{
      w.document.open();
      w.document.write('<!doctype html><html lang="es"><head><meta charset="utf-8"><title>Preparando etiqueta</title><style>body{font-family:Arial,sans-serif;background:#f4f8fc;color:#07152f;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}.box{background:#fff;border:1px solid #dbe5f1;border-radius:18px;padding:22px 26px;box-shadow:0 16px 36px rgba(15,35,65,.12);text-align:center}.spin{width:34px;height:34px;border-radius:50%;border:4px solid #dbeafe;border-top-color:#0f4c81;margin:0 auto 12px;animation:r 1s linear infinite}@keyframes r{to{transform:rotate(360deg)}}b{display:block;font-size:18px;margin-bottom:4px}</style></head><body><div class="box"><div class="spin"></div><b>Preparando etiqueta</b><span>Espere un momento...</span></div></body></html>');
      w.document.close();
    }catch(e){}
  }
  return w;
}
function printLabelInBrowser(labelId, preparedWindow=null){
  if(!labelId) return;
  const url = `${API_ENDPOINT}?label_id=${encodeURIComponent(labelId)}&autoprint=1&autoclose=1`;
  let w = preparedWindow;
  if(!w || w.closed){
    w = window.open(url, 'botella_label_print_' + Date.now(), 'popup=yes,width=760,height=560,left=80,top=80');
  } else {
    try{ w.location.href = url; }catch(e){ w = window.open(url, '_blank'); }
  }
  if(w){
    try{ w.focus(); }catch(e){}
  } else {
    appNotify('El navegador bloqueó la ventana de impresión. Permite ventanas emergentes para este sitio o abre la etiqueta desde el historial.','warning','Impresión bloqueada',false);
  }
}
function closePreparedPrintWindow(w){
  if(w && !w.closed){ try{ w.close(); }catch(e){} }
}
function fillSelect(sel, rows, valueKey, labelFn, empty='Selecciona') { sel.innerHTML = `<option value="">${empty}</option>` + rows.map(r=>`<option value="${r[valueKey]}">${labelFn(r)}</option>`).join(''); }
function selectHasValue(sel, value){ return !!sel && [...sel.options].some(o=>String(o.value)===String(value) && String(value)!==''); }
function setSelectIfExists(sel, value){ if(selectHasValue(sel,value)) { sel.value=String(value); return true; } return false; }
function alm01WarehouseId(){
  const wh = (state.warehouses||[]).find(w=>String(w.code||'').trim().toUpperCase()==='ALM01');
  return wh ? String(wh.id) : ((state.warehouses&&state.warehouses[0]) ? String(state.warehouses[0].id) : '');
}
function saveCaptureSelection(){
  try{
    localStorage.setItem('botella_cap_session', $('capSession')?.value || '');
    localStorage.setItem('botella_cap_product', $('capProduct')?.value || '');
    localStorage.setItem('botella_cap_packaging', $('capPackaging')?.value || '');
  }catch(e){}
}
function restoreCaptureBase(prev={}){
  const session = prev.session || localStorage.getItem('botella_cap_session') || '';
  const product = prev.product || localStorage.getItem('botella_cap_product') || '';
  setSelectIfExists($('capSession'), session);
  setSelectIfExists($('capProduct'), product);
  const alm01 = alm01WarehouseId();
  if(alm01) $('capWarehouse').value = alm01;
}
function restoreEndBase(prev={}){
  const product = prev.endProduct || localStorage.getItem('botella_end_product') || '';
  setSelectIfExists($('endProduct'), product);
  const alm01 = alm01WarehouseId();
  if(alm01) $('endWarehouse').value = alm01;
}

async function loadAll(){
  try{
    const prev = {
      session: $('capSession')?.value || '',
      product: $('capProduct')?.value || '',
      packaging: $('capPackaging')?.value || '',
      endProduct: $('endProduct')?.value || '',
      endPackaging: $('endPackaging')?.value || ''
    };
    const d = await api('bootstrap'); state = {...state, ...d};
    $('kDate').textContent=d.today; $('kShift').textContent='T'+d.current_shift;
    $('kProduced').textContent=fmt(d.summary.produced); $('kProgress').textContent=(d.summary.progress||0).toFixed(1)+'%'; $('barProgress').style.width=Math.min(100,d.summary.progress||0)+'%';
    renderLines();
    fillSelect($('startLine'), state.lines, 'id', r=>`${r.line_code||('L'+r.line_no)} · ${r.line_name}`);
    fillSelect($('capSession'), state.sessions, 'id', r=>`#${r.id} · L${r.production_line_no||'?'} · T${r.shift} · ${r.work_date}`,'Selecciona sesión activa');
    fillSelect($('capProduct'), state.products, 'id', r=>`${r.sku} · ${r.name}`,'Artículo');
    fillSelect($('endProduct'), state.products, 'id', r=>`${r.sku} · ${r.name}`,'Artículo última tarima');
    fillSelect($('capWarehouse'), state.warehouses, 'id', r=>`${r.code} · ${r.name}`,'Almacén');
    fillSelect($('endWarehouse'), state.warehouses, 'id', r=>`${r.code} · ${r.name}`,'Almacén');
    restoreCaptureBase(prev);
    restoreEndBase(prev);
    applyPlcMode();
    await loadPackaging('cap', prev.packaging || localStorage.getItem('botella_cap_packaging') || '');
    await loadPackaging('end', prev.endPackaging || localStorage.getItem('botella_end_packaging') || '');
    const alm01 = alm01WarehouseId();
    if(alm01) { $('capWarehouse').value = alm01; if(!$('endWarehouse').value) $('endWarehouse').value = alm01; }
    await loadLastCapture(); if(Number(state.signal_settings?.plc_enabled||0)===1){ await loadPlc(false); } else { setPlcManualMode(); }
  }catch(e){alert(e.message)}
}

let startLineTags = [];
function selectedStartMode(){ return document.querySelector('input[name="startReportMode"]:checked')?.value || 'MANUAL'; }
function setStartMode(mode){
  const manual = mode !== 'AUTOMATIC';
  if($('startModeManual')) $('startModeManual').checked = manual;
  if($('startModeAuto')) $('startModeAuto').checked = !manual;
  $('modeManualCard')?.classList.toggle('active', manual);
  $('modeAutoCard')?.classList.toggle('active', !manual);
  $('startPlcBox')?.classList.toggle('d-none', manual);
}
function renderStartTags(){
  const list = $('startPlcTagList');
  const sel = $('startPlcTag');
  if(!list || !sel) return;
  if(!startLineTags.length){
    sel.innerHTML = '<option value="">Sin TAG PLC activo para esta línea</option>';
    list.innerHTML = '<div class="tag-empty"><b>No hay TAG PLC activo configurado.</b><br>Configura production_plc_tags para esta línea o inicia el turno en modo manual.</div>';
    return;
  }
  sel.innerHTML = startLineTags.map(t=>`<option value="${Number(t.id)}">${hText(t.tag_code||('TAG #'+t.id))} · ${hText(t.product_name||'Sin artículo')} · ${hText(t.warehouse_code||'Sin almacén')}</option>`).join('');
  if(startLineTags.length===1) sel.value = String(startLineTags[0].id);
  list.innerHTML = startLineTags.map(t=>`<label class="tag-card ${String(sel.value)===String(t.id)?'selected':''}" data-tag-card="${Number(t.id)}"><input class="form-check-input me-2" type="radio" name="startTagCard" value="${Number(t.id)}" ${String(sel.value)===String(t.id)?'checked':''}> <b>${hText(t.tag_code||('TAG #'+t.id))}</b><div class="small muted ms-4">${hText(t.product_name||'Sin artículo')} · ${hText(t.warehouse_code||'Sin almacén')} · Línea ${hText(t.production_line_no||'')}</div></label>`).join('');
  document.querySelectorAll('[name="startTagCard"]').forEach(r=>r.addEventListener('change',()=>{ sel.value=r.value; renderStartTags(); }));
}
async function loadStartLineTags(lineId){
  startLineTags = [];
  if($('startPlcTag')) $('startPlcTag').innerHTML = '<option value="">Buscando TAG...</option>';
  if($('startPlcTagList')) $('startPlcTagList').innerHTML = '<div class="small muted">Buscando TAG PLC configurado para la línea...</div>';
  try{
    const d = await api('plc_tags_for_line',{query:'&line_id='+encodeURIComponent(lineId)});
    startLineTags = d.rows || [];
    renderStartTags();
    if(selectedStartMode()==='AUTOMATIC'){
      if(startLineTags.length===0) appNotify('Esta línea no tiene TAG PLC activo configurado. Puedes iniciar manual o configurar el TAG antes de iniciar automático.','warning','TAG PLC requerido',false);
      else if(startLineTags.length>1) appNotify('La línea tiene más de un TAG PLC. Selecciona cuál recibirá la señal de conteo para esta sesión.','info','Selecciona TAG PLC',true);
    }
  }catch(e){
    startLineTags=[]; renderStartTags(); appNotify(hText(e.message),'danger','No se pudieron consultar TAG PLC',false);
  }
}


let switchLineTags = [];
function manualRunningSessions(){
  return (state.sessions||[]).filter(s=>String(s.status||'RUNNING').toUpperCase()==='RUNNING' && String(s.report_mode||'MANUAL').toUpperCase()!=='AUTOMATIC');
}
function automaticRunningSessions(){
  return (state.sessions||[]).filter(s=>String(s.status||'RUNNING').toUpperCase()==='RUNNING' && String(s.report_mode||'MANUAL').toUpperCase()==='AUTOMATIC');
}

function sessionLineLabel(s){
  const line=(state.lines||[]).find(l=>Number(l.line_no)===Number(s?.production_line_no));
  return `${line?.line_name || ('Línea '+(s?.production_line_no||'?'))} · Sesión #${s?.id||''} · T${s?.shift||''} · ${s?.work_date||''}`;
}
function renderSwitchManualInfo(){
  const info=$('switchManualInfo');
  if(!info) return;
  const sessionId=$('switchManualSession')?.value || '';
  const s=(state.sessions||[]).find(x=>String(x.id)===String(sessionId));
  if(!s){
    info.innerHTML='<div class="tag-empty"><b>Sin sesión automática seleccionada.</b></div>';
    return;
  }
  const tagId=Number(s.plc_tag_id||0);
  info.innerHTML=`<div class="tag-card selected"><b>${hText(sessionLineLabel(s))}</b><div class="small muted ms-1">Modo actual: <b>AUTOMÁTICO PLC</b> · TAG seleccionado: <b>${tagId ? ('#'+tagId) : 'Sin TAG fijo'}</b></div><div class="small muted ms-1">Al confirmar, esta línea queda en modo <b>manual</b> y deja de procesar señales PLC.</div></div>`;
}
function openSwitchManualModal(){
  const sessions=automaticRunningSessions();
  if(!sessions.length){
    appNotify('No hay sesiones automáticas activas para cambiar a manual.','warning','Sin sesión automática',false);
    return;
  }
  const plcModalEl=$('modalPlc');
  if(plcModalEl){ bootstrap.Modal.getOrCreateInstance(plcModalEl).hide(); }
  $('switchManualSession').innerHTML = sessions.map(s=>`<option value="${Number(s.id)}">${hText(sessionLineLabel(s))}</option>`).join('');
  $('switchManualAuthCode').value='';
  renderSwitchManualInfo();
  bootstrap.Modal.getOrCreateInstance($('modalSwitchManual')).show();
}
function renderSwitchTags(){
  const list=$('switchTagList'), sel=$('switchPlcTag');
  if(!list || !sel) return;
  if(!switchLineTags.length){
    sel.innerHTML='<option value="">Sin TAG PLC activo para esta línea</option>';
    list.innerHTML='<div class="tag-empty"><b>No hay TAG PLC activo configurado.</b><br>Configura el TAG desde Admin para esta línea antes de cambiar a automático.</div>';
    return;
  }
  sel.innerHTML = switchLineTags.map(t=>`<option value="${Number(t.id)}">${hText(t.tag_code||('TAG #'+t.id))} · ${hText(t.product_name||'Sin artículo')} · ${hText(t.warehouse_code||'Sin almacén')}</option>`).join('');
  if(switchLineTags.length===1) sel.value=String(switchLineTags[0].id);
  list.innerHTML = switchLineTags.map(t=>`<label class="tag-card ${String(sel.value)===String(t.id)?'selected':''}" data-switch-tag-card="${Number(t.id)}"><input class="form-check-input me-2" type="radio" name="switchTagCard" value="${Number(t.id)}" ${String(sel.value)===String(t.id)?'checked':''}> <b>${hText(t.tag_code||('TAG #'+t.id))}</b><div class="small muted ms-4">${hText(t.tag_name||'')} · ${hText(t.product_name||'Sin artículo')} · ${hText(t.warehouse_code||'Sin almacén')} · Línea ${hText(t.production_line_no||'')}</div></label>`).join('');
  document.querySelectorAll('[name="switchTagCard"]').forEach(r=>r.addEventListener('change',()=>{ sel.value=r.value; renderSwitchTags(); }));
}
async function loadSwitchTagsForSession(sessionId){
  switchLineTags=[];
  const ss=(state.sessions||[]).find(s=>String(s.id)===String(sessionId));
  const line=(state.lines||[]).find(l=>Number(l.line_no)===Number(ss?.production_line_no));
  if($('switchPlcTag')) $('switchPlcTag').innerHTML='<option value="">Buscando TAG...</option>';
  if($('switchTagList')) $('switchTagList').innerHTML='<div class="small muted">Buscando TAG PLC configurado para la línea...</div>';
  if(!line){ renderSwitchTags(); return; }
  try{
    const d = await api('plc_tags_for_line',{query:'&line_id='+encodeURIComponent(line.id)});
    switchLineTags = d.rows || [];
    renderSwitchTags();
    if(switchLineTags.length===0) appNotify('La línea seleccionada no tiene TAG PLC activo configurado.','warning','TAG PLC requerido',false);
    else if(switchLineTags.length>1) appNotify('Hay más de un TAG configurado para la línea. Selecciona el TAG que contará esta sesión.','info','Selecciona TAG PLC',true);
  }catch(e){ switchLineTags=[]; renderSwitchTags(); appNotify(hText(e.message),'danger','No se pudieron consultar TAG PLC',false); }
}
async function openSwitchAutoModal(){
  const sessions=manualRunningSessions();
  if(!sessions.length){
    appNotify('No hay sesiones manuales activas para cambiar a automático.','warning','Sin sesión manual',false);
    return;
  }
  $('switchSession').innerHTML = sessions.map(s=>`<option value="${Number(s.id)}">#${hText(s.id)} · Línea ${hText(s.production_line_no||'?')} · T${hText(s.shift)} · ${hText(s.work_date)}</option>`).join('');
  $('switchAuthCode').value='';
  switchLineTags=[]; renderSwitchTags();
  bootstrap.Modal.getOrCreateInstance($('modalSwitchAuto')).show();
  await loadSwitchTagsForSession($('switchSession').value);
}
async function handlePlcFloatingClick(){
  const manuals=manualRunningSessions();
  const autos=automaticRunningSessions();
  if(manuals.length){
    await openSwitchAutoModal();
    return;
  }
  if(autos.length){
    openSwitchManualModal();
    return;
  }
  if(Number(state.signal_settings?.plc_enabled||0)!==1){
    appNotify('PLC está deshabilitado y no hay una sesión activa para cambiar modo.','warning','PLC no disponible',false);
    return;
  }
  bootstrap.Modal.getOrCreateInstance($('modalPlc')).show();
  await loadPlc();
}

function lineSessionByLine(){
  const runningByLine={};
  (state.sessions||[]).forEach(s=>{runningByLine[Number(s.production_line_no)]=s});
  return runningByLine;
}
function openStartForLine(lineId){
  const line=(state.lines||[]).find(x=>String(x.id)===String(lineId));
  if(!line){ appNotify('No se pudo identificar la línea seleccionada.','danger','Línea no válida',false); return; }
  const running=lineSessionByLine()[Number(line.line_no)];
  if(running){
    setSelectIfExists($('capSession'), running.id);
    saveCaptureSelection();
    appNotify(`<b>${h(line.line_name)}</b> ya tiene turno activo.<br>Sesión <b>#${h(running.id)}</b> · Turno <b>T${h(running.shift)}</b> · ${h(running.work_date)}.<br><span class="text-muted">Se seleccionó automáticamente en captura manual.</span>`,'success','Línea en turno activo');
    return;
  }
  $('startDate').value=$('kDate')?.textContent || $('startDate').value;
  $('startShift').value=String(state.current_shift||$('startShift').value||1);
  setSelectIfExists($('startLine'), line.id);
  setStartMode('MANUAL');
  loadStartLineTags(line.id);
  bootstrap.Modal.getOrCreateInstance($('modalStart')).show();
  appToast('Selecciona Manual o Automático PLC para iniciar sesión en '+line.line_name,'warning','Esperando iniciar sesión');
}
function renderLines(){
  const runningByLine = lineSessionByLine();
  $('linesBox').innerHTML = (state.lines||[]).map(l=>{
    const s = runningByLine[Number(l.line_no)]; const run = !!s;
    const cls = run ? 'running' : 'waiting';
    const badge = run ? `<span class="badge badge-running"><i class="bi bi-play-circle me-1"></i>Línea en turno activo</span>` : `<span class="badge badge-waiting"><i class="bi bi-hourglass-split me-1"></i>Esperando iniciar sesión</span>`;
    const hint = run ? 'Clic para seleccionar esta sesión en captura.' : 'Clic para iniciar turno en esta línea.';
    return `<div class="cardx line-card ${cls} p-3" role="button" tabindex="0" onclick="openStartForLine(${Number(l.id)})" onkeydown="if(event.key==='Enter'){openStartForLine(${Number(l.id)})}">
      <div class="d-flex justify-content-between gap-2 align-items-start">
        <div>
          <div class="fw-bold"><span class="status-dot ${run?'dot-run':'dot-off'} me-2"></span>${h(l.line_name)}</div>
          <div class="muted small">${h(l.line_code||('L'+l.line_no))} · Línea ${h(l.line_no)}</div>
          <div class="line-action-hint mt-1">${hint}</div>
        </div>
        <div class="text-end">${badge}</div>
      </div>
      ${run?`<div class="mt-3 d-flex justify-content-between align-items-center gap-2 flex-wrap"><span class="small muted">Sesión #${h(s.id)} · ${h(s.work_date)} · T${h(s.shift)} · ${String(s.report_mode||'MANUAL').toUpperCase()==='AUTOMATIC'?'Automático PLC':'Manual'}</span><button class="btn btn-sm btn-outline-danger" onclick="event.stopPropagation();openEnd(${Number(s.id)})"><i class="bi bi-stop-fill"></i> Finalizar</button></div>`:''}
    </div>`
  }).join('');
}
function applyPlcMode(){
  const enabled = Number(state.signal_settings?.plc_enabled||0) === 1;
  const hasManual = typeof manualRunningSessions === 'function' && manualRunningSessions().length > 0;
  const btn = $('btnPlcFloating');
  if(btn){
    btn.classList.toggle('btn-primary', enabled || hasManual);
    btn.classList.toggle('btn-warning', hasManual);
    btn.classList.toggle('btn-secondary', !enabled && !hasManual);
    btn.title = hasManual ? 'Cambiar sesión manual a automático PLC' : (enabled ? 'PLC automático habilitado' : 'Reportaje manual: PLC deshabilitado');
  }
  if($('cfgPlcEnabled')) $('cfgPlcEnabled').checked = enabled;
  if($('cfgPollSeconds')) $('cfgPollSeconds').value = state.signal_settings?.poll_seconds || 10;
}
function setPlcManualMode(){
  if($('plcCount')) $('plcCount').textContent='OFF';
  if($('plcBody')) $('plcBody').innerHTML = '<tr><td colspan="6" class="text-center muted">Reportaje automático PLC deshabilitado. Actívalo desde Configuración.</td></tr>';
}
function selectedProductText(){
  const sel = $('capProduct');
  return sel && sel.value ? optionText(sel) : 'Selecciona artículo en captura manual';
}
function selectedWarehouseText(){
  const sel = $('capWarehouse');
  return sel && sel.value ? optionText(sel) : 'ALM01 automático';
}
function currentPlcProductionPayload(tagId){
  const productId = Number($('capProduct')?.value || 0);
  const packagingId = Number($('capPackaging')?.value || 0);
  const warehouseId = Number($('capWarehouse')?.value || alm01WarehouseId() || 0);

  if(!productId){
    throw new Error('Selecciona el artículo activo en Captura manual antes de procesar señales PLC. La misma tag reporta el artículo seleccionado.');
  }
  if(!$('capPackaging')?.value){
    throw new Error('Selecciona el embalaje activo del artículo antes de procesar señales PLC.');
  }

  return {
    tag_id: Number(tagId),
    product_id: productId,
    packaging_option_id: packagingId,
    warehouse_id: warehouseId
  };
}
async function openConfig(){
  try{ const d = await api('signal_settings'); state.signal_settings = d.settings; applyPlcMode(); }catch(e){ console.warn(e); }
}

async function loadPackaging(prefix, preferredPackaging=''){
  const pid = $(prefix+'Product').value; const sel = $(prefix+'Packaging');
  const previous = preferredPackaging || sel.value || '';
  if(!pid){sel.innerHTML='<option value="">Selecciona artículo</option>'; calcPreview(); return;}
  try{
    const d = await api('packaging_options',{query:'&product_id='+encodeURIComponent(pid)});
    fillSelect(sel,d.rows,'id',r=>`${r.packaging_name||r.packaging_type} · ${r.packages_per_pallet} paq/tar · ${r.pieces_per_package} pzas/paq`,'Embalaje');
    if(!setSelectIfExists(sel, previous) && d.rows.length) sel.value=d.rows[0].id;
    if(prefix==='cap') { localStorage.setItem('botella_cap_packaging', sel.value || ''); calcPreview(); }
    if(prefix==='end') localStorage.setItem('botella_end_packaging', sel.value || '');
  }catch(e){ sel.innerHTML='<option value="">Sin empaque</option>'; }
}
function currentPack(){ const id = $('capPackaging').value; const opt = [...$('capPackaging').options].find(o=>o.value===id); return opt?opt.textContent:''; }
function calcPreview(){
  const text = currentPack(); const m1 = text.match(/(\d+) paq\/tar/); const m2 = text.match(/(\d+) pzas\/paq/);
  const ppt = m1?Number(m1[1]):0, ppp=m2?Number(m2[1]):0;
  const pal=1, looseP=0, loosePieces=0;
  if(ppt&&ppp){ const packages=pal*ppt+looseP; const pieces=packages*ppp+loosePieces; $('calcPreview').textContent=`Cálculo: 1 tarima · ${packages.toLocaleString('es-MX')} paquetes · ${pieces.toLocaleString('es-MX')} piezas`; } else $('calcPreview').textContent='Selecciona artículo y empaque para calcular.';
}
function captureLabelButton(r){
  return r.label_id ? `<a class="btn btn-sm btn-outline-primary" target="_blank" href="${API_ENDPOINT}?label_id=${r.label_id}">${r.label_code||'Etiqueta'}</a>` : '<span class="text-muted">Sin etiqueta</span>';
}
function captureRow(r){
  return `<tr><td>${r.entry_time||''}</td><td>L${r.production_line_no||''}</td><td>${r.sku||''} · ${r.product_name||''}</td><td>${fmt(r.pallets_qty)}</td><td>${fmt(r.packages_qty)}</td><td class="fw-bold">${fmt(r.qty_total)}</td><td>${r.source_type||'MANUAL'}</td><td>${r.warehouse_code||''}</td><td>${captureLabelButton(r)}</td></tr>`;
}
async function loadLastCapture(){
  try{
    const workDate = $('kDate').textContent;
    const d = await api('captures',{query:'&work_date='+encodeURIComponent(workDate)+'&limit=1'});
    const r = (d.rows||[])[0];
    if(!r){
      $('lastLabelBox').innerHTML = '<div class="text-muted">Sin etiqueta impresa en el día seleccionado.</div>';
      return;
    }
    $('lastLabelBox').innerHTML = `<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap"><div><div class="small text-uppercase text-muted fw-bold">Última tarima registrada</div><div class="h5 fw-bold mb-1">${r.label_code||'Sin etiqueta'}</div><div class="small text-muted">${r.entry_time||''} · Línea ${r.production_line_no||''} · ${r.source_type||'MANUAL'}</div><div class="mt-2"><b>${r.sku||''}</b> · ${r.product_name||''}</div><div class="small text-muted">${fmt(r.pallets_qty)} tarima(s) · ${fmt(r.packages_qty)} paquetes · ${fmt(r.qty_total)} piezas · ${r.warehouse_code||''}</div></div><div>${captureLabelButton(r)}</div></div>`;
  }catch(e){ console.warn(e); }
}
async function openCaptureHistory(){
  const today = $('kDate').textContent;
  if($('historyFrom') && !$('historyFrom').value) $('historyFrom').value = today;
  if($('historyTo') && !$('historyTo').value) $('historyTo').value = today;
  const modal = new bootstrap.Modal($('modalCaptureHistory'));
  modal.show();
  await loadCaptureHistory();
}
async function loadCaptureHistory(){
  try{
    const from = $('historyFrom')?.value || $('kDate').textContent;
    const to = $('historyTo')?.value || from;
    const d = await api('captures',{query:'&date_from='+encodeURIComponent(from)+'&date_to='+encodeURIComponent(to)+'&limit=500'});
    const rows = d.rows || [];
    $('historyBody').innerHTML = rows.map(captureRow).join('') || '<tr><td colspan="9" class="text-center text-muted">Sin tarimas en el rango seleccionado.</td></tr>';
    if($('historyCount')) $('historyCount').textContent = `${rows.length} registro(s) encontrados`;
  }catch(e){ alert(e.message); }
}
async function loadPlc(showErrors=true){
  if(Number(state.signal_settings?.plc_enabled||0)!==1){ setPlcManualMode(); return; }
  try{
    const d = await api('plc_pending');
    if(d.manual_mode){
      state.signal_settings = d.settings || state.signal_settings;
      applyPlcMode();
      setPlcManualMode();
      return;
    }

    const rows = d.rows || [];
    $('plcCount').textContent = rows.length;
    $('plcBody').innerHTML = rows.map(r=>`<tr>
      <td><b>${r.tag_code}</b><div class="small muted">${r.tag_name||''}</div></td>
      <td>L${r.production_line_no||''}<div class="small muted">${r.configured_line_name||r.line_name||''}</div></td>
      <td><span class="badge bg-primary-subtle text-primary border">Selección pantalla</span><div class="small muted">${selectedProductText()}</div></td>
      <td>${selectedWarehouseText()}</td>
      <td><span class="badge bg-info text-dark">Procesando automático</span></td>
      <td>
        <button class="btn btn-sm btn-success me-1" onclick="plcConfirm(${r.id}, true)"><i class="bi bi-check2-circle"></i> Procesar ahora</button>
        <button class="btn btn-sm btn-outline-danger" onclick="plcMaint(${r.id})"><i class="bi bi-tools"></i> Mantenimiento</button>
      </td>
    </tr>`).join('') || '<tr><td colspan="6" class="text-center muted">Sin señales pendientes</td></tr>';

    if(rows.length > 0){
      await plcAutoProcessRows(rows);
    }
  }catch(e){
    if(showErrors) alert(e.message);
  }
}

async function plcAutoProcessRows(rows){
  if(botellaPlcAutoProcessing) return;
  botellaPlcAutoProcessing = true;
  try{
    for(const r of rows){
      const tagId = Number(r.id || 0);
      if(!tagId) continue;

      /*
       * Cada tarima nueva vuelve a usar el mismo TAG y signal_state=1.
       * Antes el candado usaba solamente "tagId:signal_state" (ej. "5:1"),
       * por eso después de la primera tarima el navegador ignoraba las siguientes
       * hasta presionar F5, porque el Set seguía recordando la misma llave.
       *
       * Ahora la llave incluye updated_at/handled_at/consumed_at para que cada
       * nueva activación de VB sea tomada como un evento diferente.
       */
      const signalStamp = String(r.updated_at || r.handled_at || r.consumed_at || r.created_at || Date.now());
      const key = String(tagId) + ':' + signalStamp;

      if(botellaPlcAutoProcessed.has(key)) continue;
      botellaPlcAutoProcessed.add(key);

      try{
        const d = await api('plc_confirm_production', {method:'POST', body:currentPlcProductionPayload(tagId)});
        // MODO AUTOMÁTICO PLC:
        // No se abre ventana ni se manda imprimir desde navegador.
        // No se muestra modal/toast automático para evitar bloqueos visuales.
        // Solo se refrescan contadores y última tarima registrada.
        console.log('PLC automático: producción registrada', d.capture?.label_code || '');
      }catch(e){
        botellaPlcAutoProcessed.delete(key);
        appNotify(h(e.message), 'danger', 'No se pudo procesar señal PLC automática', false);
        break;
      }
    }
    await loadAll();
  }finally{
    botellaPlcAutoProcessing = false;
    if(botellaPlcAutoProcessed.size > 200) botellaPlcAutoProcessed = new Set();
  }
}

async function plcConfirm(id, skipPrompt=false){
  if(!skipPrompt){
    const ok = await appConfirm('¿Confirmar esta señal como <b>producción Botella</b>?<br><span class="text-muted">Se generará producción, etiqueta y envío automático a impresión.</span>','Confirmar señal PLC','Confirmar producción');
    if(!ok) return;
  }
  const printWin = skipPrompt ? null : prepareLabelPrintWindow();
  try{
    const d=await api('plc_confirm_production',{method:'POST',body:currentPlcProductionPayload(id)});
    printLabelInBrowser(d.capture.label_id, printWin);
    appNotify('Producción registrada y etiqueta enviada a impresión:<br><b>'+h(d.capture.label_code)+'</b>','success','Etiqueta generada');
    await loadAll();
  }catch(e){
    closePreparedPrintWindow(printWin);
    appNotify(h(e.message),'danger','No se pudo confirmar PLC',false);
  }
}
async function plcMaint(id){
  const notes = await appPrompt('Motivo / nota de mantenimiento','Señal sin producción','Mantenimiento / señal sin producción');
  if(notes===null) return;
  try{await api('plc_maintenance',{method:'POST',body:{tag_id:id,notes}}); appNotify('La señal fue enviada a mantenimiento.','success','PLC actualizado'); await loadPlc();}catch(e){appNotify(h(e.message),'danger','No se pudo actualizar PLC',false)}
}
function optionText(sel){ const opt = sel ? [...sel.options].find(o=>o.value===sel.value) : null; return opt ? opt.textContent.trim() : ''; }
function calcEndPreview(){
  const packText = optionText($('endPackaging'));
  const m1 = packText.match(/(\d+) paq\/tar/);
  const m2 = packText.match(/(\d+) pzas\/paq/);
  const ppp = m2 ? Number(m2[1]) : 0;
  const loosePackages = Number($('endLoosePackages')?.value || 0);
  const loosePieces = Number($('endLoosePieces')?.value || 0);
  const totalPieces = (loosePackages * ppp) + loosePieces;
  const msg = (loosePackages > 0 || loosePieces > 0)
    ? `Última tarima: ${loosePackages.toLocaleString('es-MX')} paquetes sueltos · ${loosePieces.toLocaleString('es-MX')} piezas sueltas · Total ${totalPieces.toLocaleString('es-MX')} piezas`
    : 'Última tarima sin paquetes ni piezas sueltas capturadas. Solo se cerrará el turno y se generará el reporte.';
  if($('endCalcPreview')) $('endCalcPreview').textContent = msg;
}
async function openEnd(id){
  const productId = $('capProduct')?.value || localStorage.getItem('botella_cap_product') || '';
  const packagingId = $('capPackaging')?.value || localStorage.getItem('botella_cap_packaging') || '';
  if(!productId){ appNotify('Selecciona primero el artículo con el que estás trabajando para poder finalizar el turno.','warning','Artículo requerido',false); return; }
  $('endSession').value=id;
  setSelectIfExists($('endProduct'), productId);
  const alm01 = alm01WarehouseId();
  if(alm01) $('endWarehouse').value = alm01;
  await loadPackaging('end', packagingId);
  if(packagingId) setSelectIfExists($('endPackaging'), packagingId);
  $('endLoosePackages').value = 0;
  $('endLoosePieces').value = 0;
  $('endNotes').value = '';
  const productText = optionText($('endProduct')) || 'Artículo no seleccionado';
  const packText = optionText($('endPackaging')) || 'Embalaje no seleccionado';
  if($('endWorkInfo')) $('endWorkInfo').textContent = `${productText} · ${packText}`;
  calcEndPreview();
  new bootstrap.Modal($('modalEnd')).show();
}
$('capProduct').addEventListener('change',()=>{ saveCaptureSelection(); loadPackaging('cap'); }); $('capSession').addEventListener('change',saveCaptureSelection); $('capPackaging').addEventListener('change',()=>{ saveCaptureSelection(); calcPreview(); }); $('endProduct').addEventListener('change',()=>{ try{localStorage.setItem('botella_end_product',$('endProduct').value||'')}catch(e){} loadPackaging('end'); }); $('endPackaging').addEventListener('change',()=>{ try{localStorage.setItem('botella_end_packaging',$('endPackaging').value||'')}catch(e){} calcEndPreview(); }); ['capPallets','capLoosePackages','capLoosePieces'].forEach(id=>$(id).addEventListener('input',calcPreview)); ['endLoosePackages','endLoosePieces'].forEach(id=>$(id)?.addEventListener('input',calcEndPreview));
document.querySelectorAll('input[name="startReportMode"]').forEach(r=>r.addEventListener('change',()=>{ setStartMode(r.value); if(r.value==='AUTOMATIC') loadStartLineTags($('startLine')?.value||0); }));
$('startLine')?.addEventListener('change',()=>loadStartLineTags($('startLine').value));
$('startPlcTag')?.addEventListener('change',renderStartTags);
$('switchSession')?.addEventListener('change',()=>loadSwitchTagsForSession($('switchSession').value));
$('switchPlcTag')?.addEventListener('change',renderSwitchTags);
$('formSwitchAuto')?.addEventListener('submit',async ev=>{
  ev.preventDefault();
  try{
    const body={session_id:Number($('switchSession').value||0),plc_tag_id:Number($('switchPlcTag').value||0),auth_code:String($('switchAuthCode').value||'').trim()};
    if(!body.session_id) throw new Error('Selecciona la sesión/línea activa.');
    if(!body.plc_tag_id) throw new Error('Selecciona el TAG PLC configurado para la línea.');
    if(!body.auth_code) throw new Error('Captura el código de autorización.');
    const ok = await appConfirm('¿Cambiar esta sesión a <b>Automático PLC</b>?<br><span class="text-muted">A partir de este momento la línea escuchará el TAG seleccionado.</span>','Confirmar cambio a automático','Cambiar a automático');
    if(!ok) return;
    const d = await api('switch_session_to_automatic',{method:'POST',body});
    bootstrap.Modal.getOrCreateInstance($('modalSwitchAuto')).hide();
    appNotify(hText(d.message||'Sesión cambiada a automático PLC.'),'success','Modo automático activado');
    await loadAll();
    bootstrap.Modal.getOrCreateInstance($('modalPlc')).show();
    await loadPlc(false);
  }catch(e){ appNotify(hText(e.message),'danger','No se pudo cambiar a automático',false); }
});
$('switchManualSession')?.addEventListener('change',renderSwitchManualInfo);
$('formSwitchManual')?.addEventListener('submit',async ev=>{
  ev.preventDefault();
  try{
    const body={session_id:Number($('switchManualSession').value||0),auth_code:String($('switchManualAuthCode').value||'').trim()};
    if(!body.session_id) throw new Error('Selecciona la sesión/línea automática.');
    if(!body.auth_code) throw new Error('Captura el código de autorización.');
    const ok = await appConfirm('¿Cambiar esta sesión a <b>Manual</b>?<br><span class="text-muted">La línea dejará de escuchar señales PLC y quedará disponible para captura manual.</span>','Confirmar cambio a manual','Cambiar a manual');
    if(!ok) return;
    const d = await api('switch_session_to_manual',{method:'POST',body});
    bootstrap.Modal.getOrCreateInstance($('modalSwitchManual')).hide();
    appNotify(hText(d.message||'Sesión cambiada a manual.'),'success','Modo manual activado');
    await loadAll();
  }catch(e){ appNotify(hText(e.message),'danger','No se pudo cambiar a manual',false); }
});

$('formStart').addEventListener('submit',async ev=>{
  ev.preventDefault();
  try{
    const mode = selectedStartMode();
    const body = {work_date:$('startDate').value,shift:Number($('startShift').value),production_line_id:Number($('startLine').value),report_mode:mode,plc_tag_id:0};
    if(mode==='AUTOMATIC'){
      body.plc_tag_id = Number($('startPlcTag')?.value || 0);
      if(!body.plc_tag_id) throw new Error('Selecciona el TAG PLC que se utilizará para recibir la señal de conteo.');
    }
    const d=await api('start_shift',{method:'POST',body});
    bootstrap.Modal.getInstance($('modalStart')).hide();
    appNotify(hText(d.message||'Turno iniciado correctamente.'),'success','Sesión iniciada');
    await loadAll();
  }catch(e){appNotify(hText(e.message),'danger','No se pudo iniciar turno',false)}
});
$('formCapture').addEventListener('submit',async ev=>{
  ev.preventDefault();
  const printWin = prepareLabelPrintWindow();
  try{
    saveCaptureSelection();
    const saved={session:$('capSession').value,product:$('capProduct').value,packaging:$('capPackaging').value};
    const d=await api('manual_capture',{method:'POST',body:{session_id:Number($('capSession').value),product_id:Number($('capProduct').value),warehouse_id:Number($('capWarehouse').value||alm01WarehouseId()||0),packaging_option_id:Number($('capPackaging').value||0),pallets_qty:1,loose_packages:0,loose_pieces:0,notes:$('capNotes').value}});
    printLabelInBrowser(d.capture.label_id, printWin);
    appNotify('Producción registrada y etiqueta enviada a impresión:<br><b>'+h(d.capture.label_code)+'</b>','success','Etiqueta generada');
    $('capPallets').value=1;$('capLoosePackages').value=0;$('capLoosePieces').value=0;$('capNotes').value='';
    await loadAll();
    setSelectIfExists($('capSession'), saved.session);
    setSelectIfExists($('capProduct'), saved.product);
    if(alm01WarehouseId()) $('capWarehouse').value=alm01WarehouseId();
    await loadPackaging('cap', saved.packaging);
  }catch(e){
    closePreparedPrintWindow(printWin);
    alert(e.message);
  }
});
$('formEnd').addEventListener('submit',async ev=>{
  ev.preventDefault();
  const printWin = prepareLabelPrintWindow();
  try{
    const loosePackages=Number($('endLoosePackages').value||0);
    const loosePieces=Number($('endLoosePieces').value||0);
    if(loosePackages<0||loosePieces<0){throw new Error('Paquetes y piezas sueltas no pueden ser negativos.');}
    const body={session_id:Number($('endSession').value),product_id:Number($('endProduct').value||0),warehouse_id:Number($('endWarehouse').value||alm01WarehouseId()||0),packaging_option_id:Number($('endPackaging').value||0),loose_packages:loosePackages,loose_pieces:loosePieces,notes:$('endNotes').value};
    if(!body.product_id){throw new Error('No se encontró artículo activo para cierre. Selecciónalo en captura manual antes de finalizar turno.');}
    if(loosePackages<=0 && loosePieces<=0){throw new Error('Captura paquetes sueltos o piezas sueltas para generar la etiqueta de última tarima incompleta.');}
    const d=await api('end_shift',{method:'POST',body});
    bootstrap.Modal.getInstance($('modalEnd')).hide();
    if(d.capture&&d.capture.label_id){
      printLabelInBrowser(d.capture.label_id, printWin);
    } else {
      closePreparedPrintWindow(printWin);
    }
    if(d.report_url) setTimeout(()=>window.open(d.report_url,'_blank'),600);
    await loadAll();
  }catch(e){
    closePreparedPrintWindow(printWin);
    alert(e.message);
  }
});
$('formTarget')?.addEventListener('submit',async ev=>{ev.preventDefault(); try{await api('set_target',{method:'POST',body:{work_date:$('targetDate').value,target_pieces:Number($('targetPieces').value||0),notes:$('targetNotes').value}}); bootstrap.Modal.getInstance($('modalTarget')).hide(); await loadAll();}catch(e){alert(e.message)}});
$('formConfig')?.addEventListener('submit',async ev=>{ev.preventDefault(); try{const d=await api('save_signal_settings',{method:'POST',body:{plc_enabled:$('cfgPlcEnabled').checked?1:0,poll_seconds:Number($('cfgPollSeconds').value||10)}}); state.signal_settings=d.settings; applyPlcMode(); if(Number(state.signal_settings.plc_enabled||0)===1){ await loadPlc(false); } else { setPlcManualMode(); } bootstrap.Modal.getInstance($('modalConfig'))?.hide();}catch(e){alert(e.message)}});
loadAll();
let botellaPlcPollTimer = setInterval(()=>{
  // En producción automática no detenemos la lectura si la pestaña queda en segundo plano.
  // Esto evita que la línea deje de procesar tarimas cuando el navegador pierde foco.
  if(Number(state.signal_settings?.plc_enabled||0)===1) loadPlc(false);
}, Math.max(5000, Number(state.signal_settings?.poll_seconds||10) * 1000));
window.addEventListener('pagehide',()=>{ try{ clearInterval(botellaPlcPollTimer); }catch(e){} });
window.addEventListener('beforeunload',()=>{ try{ clearInterval(botellaPlcPollTimer); }catch(e){} });
</script>
</body>
</html>
