<?php
/**
 * produccion.php (v4.4)
 * ============================================================
 * CAMBIOS:
 * - Bitácora de capturas por TURNO seleccionado/activo
 * - Totales por almacén por TURNO seleccionado/activo
 * - Merma por turno
 * - Corrige bitácora vacía cuando production_capture_groups no tiene datos
 * - Fallback legacy desde production_entries + quality_entries
 * - Corrige visibilidad de combobox/select en tema oscuro
 * - Mejora visual de grids/tablas/botones
 * - Agrega reporte de cierre de turno imprimible
 * - Agrega PDF simple de cierre con resumen de barras y conversiones
 * - Agrega formato imprimible con firmas: Líder / Supervisor / Calidad
 * - Agrega simulador visual de consumo de agua por turno
 * - Ajusta diseño general a fondo blanco
 * - Cards de turno clicables con resumen, estado en curso y descarga de reporte
 * - Botón flotante Manual/Automático para señal PLC Hielo Barra
 * - Oculta opción de mantenimiento en modal de señal PLC
 * ============================================================
 */

declare(strict_types=1);

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();

// Hielo Barra trabaja en Empresa 1 / Localidad 1.
// Se fuerza aquí para que la lectura PLC no apunte a Botella u otra planta por scope activo.
$companyId  = 1;
$locationId = 1;
$action     = (string)($_GET['action'] ?? '');

$availableTimezones = [
  'America/Matamoros'   => 'Reynosa / Matamoros',
  'America/Monterrey'   => 'Monterrey',
  'America/Mexico_City' => 'CDMX',
  'America/Chihuahua'   => 'Chihuahua',
  'America/Tijuana'     => 'Tijuana',
  'UTC'                 => 'UTC',
];

if (!isset($_SESSION['breport_tz']) || !is_string($_SESSION['breport_tz']) || $_SESSION['breport_tz'] === '') {
  $_SESSION['breport_tz'] = 'America/Matamoros';
}
if (!isset($availableTimezones[$_SESSION['breport_tz']])) {
  $_SESSION['breport_tz'] = 'America/Matamoros';
}
$APP_TZ = $_SESSION['breport_tz'];

// Regla operativa de cosecha: cada disparo/señal debe reportar exactamente 10 barras.
const PRODUCTION_HARVEST_QTY = 10.0;

// Hielo Barra: tabla oficial PLC para cosecha de barras.
// Este módulo NO usa tags de Botella y NO amarra una tag fija:
// lee cualquier tag activa dada de alta para Empresa 1 / Localidad 1.
const HB_PLC_TAG_TABLE = 'production_plc_tags';

function jexit($data, int $code = 200): void {
  http_response_code($code);
  header('Content-Type: application/json; charset=utf-8');
  echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  exit;
}
function read_json(): array {
  $raw = file_get_contents('php://input');
  $d = json_decode($raw ?: '[]', true);
  return is_array($d) ? $d : [];
}
function need_csrf(): void {
  if (($_SERVER['REQUEST_METHOD'] ?? '') === 'GET') return;
  $hdr = $_SERVER['HTTP_X_CSRF'] ?? '';
  if ($hdr === '') jexit(['ok'=>false,'error'=>'Falta X-CSRF'], 419);
  csrf_verify($hdr);
}

if ($action !== '') {
  header('Content-Type: application/json; charset=utf-8');
  set_error_handler(function($severity,$message,$file,$line){
    throw new ErrorException($message, 0, $severity, $file, $line);
  });
  register_shutdown_function(function(){
    $e = error_get_last();
    if ($e && in_array($e['type'], [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR], true)) {
      http_response_code(500);
      echo json_encode(['ok'=>false,'error'=>'PHP_FATAL','detail'=>$e], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
  });
}

function 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 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 ensure_production_downtime_schema(PDO $pdo): void {
  /*
   * Historial de caídas por mantenimiento originadas por señal PLC.
   * Esta tabla no genera producción ni inventario; solo guarda la decisión
   * del operador cuando el tag cambia de 0 a 1 y no corresponde reportar barra.
   */
  if (!table_exists($pdo, 'production_downtime_events')) {
    $pdo->exec("
      CREATE TABLE production_downtime_events (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        company_id BIGINT NOT NULL,
        location_id BIGINT NOT NULL,
        work_date DATE NOT NULL,
        shift TINYINT NOT NULL,
        tag_id BIGINT NULL,
        tag_code VARCHAR(80) NULL,
        event_time DATETIME NOT NULL,
        reason VARCHAR(120) NOT NULL DEFAULT 'MANTENIMIENTO',
        notes VARCHAR(255) NULL,
        created_by BIGINT NOT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY idx_pde_scope_date (company_id, location_id, work_date, shift),
        KEY idx_pde_tag (company_id, location_id, tag_id),
        KEY idx_pde_event_time (event_time)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");
    return;
  }

  $cols = [
    'company_id'  => "ADD COLUMN company_id BIGINT NOT NULL DEFAULT 0 AFTER id",
    'location_id' => "ADD COLUMN location_id BIGINT NOT NULL DEFAULT 0 AFTER company_id",
    'work_date'   => "ADD COLUMN work_date DATE NOT NULL DEFAULT (CURRENT_DATE) AFTER location_id",
    'shift'       => "ADD COLUMN shift TINYINT NOT NULL DEFAULT 1 AFTER work_date",
    'tag_id'      => "ADD COLUMN tag_id BIGINT NULL AFTER shift",
    'tag_code'    => "ADD COLUMN tag_code VARCHAR(80) NULL AFTER tag_id",
    'event_time'  => "ADD COLUMN event_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER tag_code",
    'reason'      => "ADD COLUMN reason VARCHAR(120) NOT NULL DEFAULT 'MANTENIMIENTO' AFTER event_time",
    'notes'       => "ADD COLUMN notes VARCHAR(255) NULL AFTER reason",
    'created_by'  => "ADD COLUMN created_by BIGINT NOT NULL DEFAULT 0 AFTER notes",
    'created_at'  => "ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER created_by",
  ];
  foreach ($cols as $col => $ddl) {
    if (!col_exists($pdo, 'production_downtime_events', $col)) {
      $pdo->exec("ALTER TABLE production_downtime_events {$ddl}");
    }
  }

  try { $pdo->exec("ALTER TABLE production_downtime_events ADD KEY idx_pde_scope_date (company_id, location_id, work_date, shift)"); } catch (Throwable $e) {}
  try { $pdo->exec("ALTER TABLE production_downtime_events ADD KEY idx_pde_tag (company_id, location_id, tag_id)"); } catch (Throwable $e) {}
  try { $pdo->exec("ALTER TABLE production_downtime_events ADD KEY idx_pde_event_time (event_time)"); } catch (Throwable $e) {}
}

function ensure_capture_schema(PDO $pdo): void {
  if (!table_exists($pdo, 'production_capture_groups')) {
    $pdo->exec("
      CREATE TABLE production_capture_groups (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        capture_uid VARCHAR(64) NOT NULL,
        company_id BIGINT NOT NULL,
        location_id BIGINT NOT NULL,
        work_date DATE NOT NULL,
        shift TINYINT NOT NULL,
        product_id BIGINT NOT NULL,
        entry_time DATETIME NOT NULL,
        cf_wh_id BIGINT NOT NULL,
        qty_total DECIMAL(14,3) NOT NULL DEFAULT 0,
        qty_cf DECIMAL(14,3) NOT NULL DEFAULT 0,
        qty_cub DECIMAL(14,3) NOT NULL DEFAULT 0,
        qty_merma DECIMAL(14,3) NOT NULL DEFAULT 0,
        created_by BIGINT NOT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_by BIGINT NULL,
        updated_at DATETIME NULL DEFAULT NULL,
        PRIMARY KEY (id),
        UNIQUE KEY uq_capture_uid (capture_uid),
        KEY idx_cg_scope_date (company_id, location_id, work_date),
        KEY idx_cg_shift (company_id, location_id, work_date, shift),
        KEY idx_cg_entry_time (entry_time)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");
  }

  if (table_exists($pdo, 'production_entries') && !col_exists($pdo, 'production_entries', 'capture_uid')) {
    $pdo->exec("ALTER TABLE production_entries ADD COLUMN capture_uid VARCHAR(64) NULL AFTER entry_time");
    $pdo->exec("CREATE INDEX idx_production_entries_capture_uid ON production_entries(capture_uid)");
  }

  if (table_exists($pdo, 'quality_entries') && !col_exists($pdo, 'quality_entries', 'capture_uid')) {
    $pdo->exec("ALTER TABLE quality_entries ADD COLUMN capture_uid VARCHAR(64) NULL AFTER notes");
    $pdo->exec("CREATE INDEX idx_quality_entries_capture_uid ON quality_entries(capture_uid)");
  }
}


function ensure_signal_detection_schema(PDO $pdo): void {
  ensure_production_downtime_schema($pdo);
  if (!table_exists($pdo, 'production_signal_settings')) {
    $pdo->exec(""
      . "CREATE TABLE production_signal_settings ("
      . "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
      . "company_id BIGINT NOT NULL,"
      . "location_id BIGINT NOT NULL,"
      . "report_mode ENUM('MANUAL','AUTOMATIC') NOT NULL DEFAULT 'MANUAL',"
      . "plc_enabled TINYINT(1) NOT NULL DEFAULT 0,"
      . "poll_seconds INT NOT NULL DEFAULT 5,"
      . "default_product_id BIGINT NULL,"
      . "default_cf_wh_id BIGINT NULL,"
      . "default_total_qty DECIMAL(14,3) NOT NULL DEFAULT 10.000,"
      . "updated_by BIGINT NULL,"
      . "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
      . "updated_at DATETIME NULL DEFAULT NULL,"
      . "PRIMARY KEY (id),"
      . "UNIQUE KEY uq_prod_signal_scope (company_id, location_id)"
      . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
    );
  }

  /*
   * Tabla oficial para la detección de señal PLC.
   * Nota importante:
   * En la base ya existe production_plc_tags y ahí se están dando de alta los tags.
   * Por eso el módulo debe leer esta tabla, no production_signal_tags.
   */
  if (!table_exists($pdo, 'production_plc_tags')) {
    $pdo->exec(""
      . "CREATE TABLE production_plc_tags ("
      . "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
      . "company_id BIGINT NOT NULL,"
      . "location_id BIGINT NOT NULL,"
      . "tag_code VARCHAR(80) NOT NULL,"
      . "tag_name VARCHAR(120) NOT NULL,"
      . "plc_name VARCHAR(120) NULL,"
      . "ip_address VARCHAR(80) NULL,"
      . "line_name VARCHAR(80) NULL,"
      . "product_id BIGINT NULL,"
      . "cf_wh_id BIGINT NULL,"
      . "qty_per_signal DECIMAL(14,3) NOT NULL DEFAULT 1.000,"
      . "signal_state TINYINT(1) NOT NULL DEFAULT 0,"
      . "status ENUM('IDLE','IN_PROCESS','RESOLVED','DISABLED') NOT NULL DEFAULT 'IDLE',"
      . "active TINYINT(1) NOT NULL DEFAULT 1,"
      . "last_value_at DATETIME NULL,"
      . "opened_at DATETIME NULL,"
      . "opened_by BIGINT NULL,"
      . "handled_at DATETIME NULL,"
      . "handled_by BIGINT NULL,"
      . "last_action ENUM('PRODUCTION','MAINTENANCE','RESET') NULL,"
      . "notes VARCHAR(255) NULL,"
      . "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
      . "updated_at DATETIME NULL DEFAULT NULL,"
      . "PRIMARY KEY (id),"
      . "UNIQUE KEY uq_prod_plc_tag (company_id, location_id, tag_code),"
      . "KEY idx_prod_plc_pending (company_id, location_id, active, signal_state, status)"
      . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
    );
  } else {
    // Completa columnas faltantes en instalaciones donde production_plc_tags ya existía.
    $cols = [
      'tag_name'       => "ADD COLUMN tag_name VARCHAR(120) NOT NULL DEFAULT '' AFTER tag_code",
      'plc_name'       => "ADD COLUMN plc_name VARCHAR(120) NULL AFTER tag_name",
      'ip_address'     => "ADD COLUMN ip_address VARCHAR(80) NULL AFTER plc_name",
      'line_name'      => "ADD COLUMN line_name VARCHAR(80) NULL AFTER ip_address",
      'product_id'     => "ADD COLUMN product_id BIGINT NULL AFTER line_name",
      'cf_wh_id'       => "ADD COLUMN cf_wh_id BIGINT NULL AFTER product_id",
      'qty_per_signal' => "ADD COLUMN qty_per_signal DECIMAL(14,3) NOT NULL DEFAULT 1.000 AFTER cf_wh_id",
      'signal_state'   => "ADD COLUMN signal_state TINYINT(1) NOT NULL DEFAULT 0 AFTER qty_per_signal",
      'status'         => "ADD COLUMN status ENUM('IDLE','IN_PROCESS','RESOLVED','DISABLED') NOT NULL DEFAULT 'IDLE' AFTER signal_state",
      'active'         => "ADD COLUMN active TINYINT(1) NOT NULL DEFAULT 1 AFTER status",
      'last_value_at'  => "ADD COLUMN last_value_at DATETIME NULL AFTER active",
      'opened_at'      => "ADD COLUMN opened_at DATETIME NULL AFTER last_value_at",
      'opened_by'      => "ADD COLUMN opened_by BIGINT NULL AFTER opened_at",
      'handled_at'     => "ADD COLUMN handled_at DATETIME NULL AFTER opened_by",
      'handled_by'     => "ADD COLUMN handled_by BIGINT NULL AFTER handled_at",
      'last_action'    => "ADD COLUMN last_action ENUM('PRODUCTION','MAINTENANCE','RESET') NULL AFTER handled_by",
      'notes'          => "ADD COLUMN notes VARCHAR(255) NULL AFTER last_action",
      'created_at'     => "ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER notes",
      'updated_at'     => "ADD COLUMN updated_at DATETIME NULL DEFAULT NULL AFTER created_at",
    ];
    foreach ($cols as $col => $ddl) {
      if (!col_exists($pdo, 'production_plc_tags', $col)) {
        $pdo->exec("ALTER TABLE production_plc_tags {$ddl}");
      }
    }

    // Índices útiles para búsqueda de señal pendiente. Si ya existen, no pasa nada crítico.
    try { $pdo->exec("ALTER TABLE production_plc_tags ADD UNIQUE KEY uq_prod_plc_tag (company_id, location_id, tag_code)"); } catch (Throwable $e) {}
    try { $pdo->exec("ALTER TABLE production_plc_tags ADD KEY idx_prod_plc_pending (company_id, location_id, active, signal_state, status)"); } catch (Throwable $e) {}
  }

  // Compatibilidad: si existían tags en production_signal_tags, se copian a production_plc_tags.
  if (table_exists($pdo, 'production_signal_tags')) {
    try {
      $pdo->exec(""
        . "INSERT INTO production_plc_tags(company_id, location_id, tag_code, tag_name, line_name, signal_state, status, active, last_value_at, created_at, updated_at) "
        . "SELECT company_id, location_id, tag_code, tag_name, line_name, signal_state, status, active, last_value_at, created_at, updated_at "
        . "FROM production_signal_tags "
        . "ON DUPLICATE KEY UPDATE "
        . "tag_name=VALUES(tag_name), line_name=VALUES(line_name), active=VALUES(active), signal_state=VALUES(signal_state), status=VALUES(status), updated_at=NOW()"
      );
    } catch (Throwable $e) {}
  }

  // Registro inicial de configuración por alcance.
  $sc = function_exists('scope') ? scope() : ['company_id'=>0,'location_id'=>0];
  $cid = (int)($sc['company_id'] ?? 0);
  $lid = (int)($sc['location_id'] ?? 0);
  if ($cid > 0 && $lid > 0) {
    $st = $pdo->prepare("SELECT id FROM production_signal_settings WHERE company_id=? AND location_id=? LIMIT 1");
    $st->execute([$cid, $lid]);
    if (!$st->fetchColumn()) {
      $ins = $pdo->prepare("INSERT INTO production_signal_settings(company_id, location_id, report_mode, plc_enabled, poll_seconds, created_at) VALUES(?, ?, 'MANUAL', 0, 5, NOW())");
      $ins->execute([$cid, $lid]);
    }
  }
}

function get_signal_settings(PDO $pdo, int $companyId, int $locationId): array {
  ensure_signal_detection_schema($pdo);
  $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);
  if ($row) return $row;
  $ins = $pdo->prepare("INSERT INTO production_signal_settings(company_id, location_id, report_mode, plc_enabled, poll_seconds, created_at) VALUES(?, ?, 'MANUAL', 0, 5, NOW())");
  $ins->execute([$companyId, $locationId]);
  $st->execute([$companyId, $locationId]);
  return $st->fetch(PDO::FETCH_ASSOC) ?: [];
}

function reset_signal_tag(PDO $pdo, int $companyId, int $locationId, int $tagId, int $userId, string $action): void {
  if ($tagId <= 0 || !table_exists($pdo, 'production_plc_tags')) return;
  $st = $pdo->prepare(""
    . "UPDATE production_plc_tags "
    . "SET signal_state=0, status='RESOLVED', handled_at=NOW(), handled_by=?, last_action=?, updated_at=NOW() "
    . "WHERE id=? AND company_id=? AND location_id=?"
  );
  $st->execute([$userId, $action, $tagId, $companyId, $locationId]);
}

function tz_now(string $tz): DateTimeImmutable {
  return new DateTimeImmutable('now', new DateTimeZone($tz));
}
function tz_today(string $tz): string {
  return tz_now($tz)->format('Y-m-d');
}
function normalize_dt_local(string $dt, string $tz): string {
  $dt = trim($dt);
  if ($dt === '') return tz_now($tz)->format('Y-m-d H:i:s');
  $dt = str_replace('T', ' ', $dt);
  if (strlen($dt) === 16) $dt .= ':00';
  return $dt;
}
function detect_shift_by_time(string $date, string $tz): int {
  $z = new DateTimeZone($tz);
  $now = new DateTimeImmutable('now', $z);
  $selectedDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date.' '.$now->format('H:i:s'), $z);
  if (!$selectedDate) return 1;
  $hm = $selectedDate->format('H:i');
  if ($hm >= '07:00' && $hm < '15:00') return 1;
  if ($hm >= '15:00' && $hm < '22:30') return 2;
  return 3;
}

function ensure_prod_day(PDO $pdo, int $companyId, int $locationId, string $date, int $userId): int {
  $st = $pdo->prepare("SELECT id FROM production_day WHERE company_id=? AND location_id=? AND prod_date=? LIMIT 1");
  $st->execute([$companyId, $locationId, $date]);
  $id = (int)($st->fetchColumn() ?: 0);
  if ($id > 0) return $id;

  $st = $pdo->prepare("
    INSERT INTO production_day(
      company_id, location_id, prod_date, created_by,
      antenna_total_qty, total_prod_qty, diff_qty, warn_flag, locked,
      target_t1, target_t2, target_t3, target_total
    )
    VALUES(?,?,?,?,0,0,0,0,0,400,400,400,1200)
  ");
  $st->execute([$companyId, $locationId, $date, $userId]);
  return (int)$pdo->lastInsertId();
}

function get_running_shift(PDO $pdo, int $companyId, int $locationId, int $userId, string $date): ?array {
  $st = $pdo->prepare("
    SELECT id, work_date, shift, start_at, last_heartbeat_at, status
    FROM shift_sessions
    WHERE company_id=? AND location_id=? AND user_id=? AND work_date=? AND status='RUNNING'
    ORDER BY id DESC
    LIMIT 1
  ");
  $st->execute([$companyId,$locationId,$userId,$date]);
  $row = $st->fetch(PDO::FETCH_ASSOC);
  return $row ?: null;
}

function get_production_shift_totals(PDO $pdo, int $companyId, int $locationId, string $date): array {
  /**
   * Totales REALES por turno para los contadores principales.
   * ------------------------------------------------------------
   * Regla:
   * - Si existe production_capture_groups para la fecha, se usa como fuente de verdad
   *   porque ahí quedó guardada la captura completa: CF + CUB + MERMA.
   * - Si no hay grupos (histórico legacy), se calcula como production_entries + merma.
   *
   * Esto evita que los contadores se inflen por registros duplicados en tablas detalle
   * y asegura que un turno con CF=118, CUB=1 y Merma=1 muestre 120, no 122.
   */
  $out = [1=>0.0, 2=>0.0, 3=>0.0];

  if (production_capture_groups_has_rows($pdo, $companyId, $locationId, $date, null)) {
    $st = $pdo->prepare("
      SELECT shift, COALESCE(SUM(qty_total),0) AS qty_total
      FROM production_capture_groups
      WHERE company_id=? AND location_id=? AND work_date=?
      GROUP BY shift
    ");
    $st->execute([$companyId, $locationId, $date]);
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
      $shift = (int)($r['shift'] ?? 0);
      if (isset($out[$shift])) $out[$shift] = (float)($r['qty_total'] ?? 0);
    }
    return $out;
  }

  $st = $pdo->prepare("
    SELECT shift, COALESCE(SUM(qty),0) AS qty_ok
    FROM production_entries
    WHERE company_id=? AND location_id=? AND work_date=?
    GROUP BY shift
  ");
  $st->execute([$companyId,$locationId,$date]);
  foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
    $shift = (int)($r['shift'] ?? 0);
    if (isset($out[$shift])) $out[$shift] += (float)($r['qty_ok'] ?? 0);
  }

  $mermaByShift = get_merma_by_shift($pdo, $companyId, $locationId, $date);
  foreach ($mermaByShift as $shift => $qty) {
    if (isset($out[(int)$shift])) $out[(int)$shift] += (float)$qty;
  }

  return $out;
}

function recalc_day(PDO $pdo, int $companyId, int $locationId, string $date): void {
  $byShift = get_production_shift_totals($pdo, $companyId, $locationId, $date);
  $total = (float)array_sum($byShift);

  $st = $pdo->prepare("SELECT id, antenna_total_qty FROM production_day WHERE company_id=? AND location_id=? AND prod_date=? LIMIT 1");
  $st->execute([$companyId,$locationId,$date]);
  $row = $st->fetch(PDO::FETCH_ASSOC);
  if (!$row) return;

  $antenna = (float)($row['antenna_total_qty'] ?? 0);
  $diff = $total - $antenna;
  $warn = (abs($diff) > 0.00001) ? 1 : 0;

  $st = $pdo->prepare("UPDATE production_day SET total_prod_qty=?, diff_qty=?, warn_flag=? WHERE id=?");
  $st->execute([$total, $diff, $warn, (int)$row['id']]);
}

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



function ensure_production_merma_warehouse(PDO $pdo, int $companyId, int $locationId): int {
  /*
    Almacén ORIGEN de merma de producción.
    Importante: NO debe usar APROV-MERMA. Ese almacén es únicamente para
    merma ya dispuesta/aprovechada por Calidad.
  */
  $st = $pdo->prepare("
    SELECT id
    FROM warehouses
    WHERE company_id=? AND location_id=? AND active=1
      AND UPPER(code) = 'MERMA'
    LIMIT 1
  ");
  $st->execute([$companyId, $locationId]);
  $id = (int)($st->fetchColumn() ?: 0);
  if ($id > 0) return $id;

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

function get_capture_group_id(PDO $pdo, int $companyId, int $locationId, string $captureUid): int {
  if ($captureUid === '' || !table_exists($pdo, 'production_capture_groups')) return 0;
  $st = $pdo->prepare("
    SELECT id
    FROM production_capture_groups
    WHERE company_id=? AND location_id=? AND capture_uid=?
    LIMIT 1
  ");
  $st->execute([$companyId, $locationId, $captureUid]);
  return (int)($st->fetchColumn() ?: 0);
}

function product_is_bar_150(PDO $pdo, int $companyId, int $productId): bool {
  $st = $pdo->prepare("
    SELECT sku, name
    FROM products
    WHERE company_id=? AND id=? AND active=1
    LIMIT 1
  ");
  $st->execute([$companyId, $productId]);
  $p = $st->fetch(PDO::FETCH_ASSOC);
  if (!$p) return false;
  $sku = mb_strtoupper(trim((string)($p['sku'] ?? '')));
  $name = mb_strtoupper(trim((string)($p['name'] ?? '')));
  return $sku === 'BARRA 150 KILOS'
    || (str_contains($sku, 'BARRA') && str_contains($sku, '150'))
    || (str_contains($name, 'BARRA') && str_contains($name, '150'));
}

function delete_production_inventory_moves_by_group(PDO $pdo, int $companyId, int $locationId, int $captureGroupId): void {
  if ($captureGroupId <= 0) return;

  // Solo se eliminan/reemplazan movimientos generados por este módulo y ligados
  // al grupo de captura. No toca salidas, ventas, calidad, cubicado ni ajustes.
  $st = $pdo->prepare("
    DELETE FROM inventory_moves
    WHERE company_id=?
      AND location_id=?
      AND ref_type IN ('PRODUCTION','PRODUCTION_CUB','PRODUCTION_MERMA')
      AND ref_id=?
  ");
  $st->execute([$companyId, $locationId, $captureGroupId]);
}

function insert_production_inventory_moves_for_capture(
  PDO $pdo,
  int $companyId,
  int $locationId,
  string $workDate,
  int $productId,
  int $cfWarehouseId,
  int $cubWarehouseId,
  float $qtyCf,
  float $qtyCub,
  float $qtyMerma,
  string $captureUid,
  int $captureGroupId,
  int $userId
): void {
  if ($captureGroupId <= 0) {
    throw new RuntimeException('No se pudo ligar la captura de producción con inventory_moves.');
  }

  if (!product_is_bar_150($pdo, $companyId, $productId)) {
    throw new RuntimeException('Este módulo de producción solo debe reportar BARRA 150 KILOS. Otros productos deben entrar a CF3 por su flujo correspondiente.');
  }

  $qtyCf = round($qtyCf, 3);
  $qtyCub = round($qtyCub, 3);
  $qtyMerma = round($qtyMerma, 3);

  $stWh = $pdo->prepare("
    SELECT code
    FROM warehouses
    WHERE id=? AND company_id=? AND location_id=? AND active=1
    LIMIT 1
  ");

  $cfCode = '';
  if ($qtyCf > 0) {
    $stWh->execute([$cfWarehouseId, $companyId, $locationId]);
    $cfCode = strtoupper(trim((string)($stWh->fetchColumn() ?: '')));
    if (!in_array($cfCode, ['CF1','CF2'], true)) {
      throw new RuntimeException('BARRA 150 KILOS de almacén directo solo puede entrar a CF1 o CF2.');
    }
  }

  $cubCode = '';
  if ($qtyCub > 0) {
    $stWh->execute([$cubWarehouseId, $companyId, $locationId]);
    $cubCode = strtoupper(trim((string)($stWh->fetchColumn() ?: '')));
    if ($cubCode !== 'CUB') {
      throw new RuntimeException('BARRA 150 KILOS destinada a cubicado solo puede entrar a CUB.');
    }
  }

  // En edición se reemplazan los movimientos anteriores ligados a esta captura.
  delete_production_inventory_moves_by_group($pdo, $companyId, $locationId, $captureGroupId);

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

  if ($qtyCf > 0) {
    $stMove->execute([
      $companyId,
      $locationId,
      $cfWarehouseId,
      $workDate,
      $productId,
      $qtyCf,
      'PRODUCTION',
      $captureGroupId,
      'Entrada real producción a ' . $cfCode . ' | Captura: ' . $captureUid,
      $userId
    ]);
  }

  if ($qtyCub > 0) {
    $stMove->execute([
      $companyId,
      $locationId,
      $cubWarehouseId,
      $workDate,
      $productId,
      $qtyCub,
      'PRODUCTION_CUB',
      $captureGroupId,
      'Entrada real producción a CUB | Captura: ' . $captureUid,
      $userId
    ]);
  }

  if ($qtyMerma > 0) {
    $mermaWhId = ensure_production_merma_warehouse($pdo, $companyId, $locationId);
    if ($mermaWhId <= 0) {
      throw new RuntimeException('No se encontró/creó almacén de merma para registrar la merma de producción.');
    }

    $stMove->execute([
      $companyId,
      $locationId,
      $mermaWhId,
      $workDate,
      $productId,
      $qtyMerma,
      'PRODUCTION_MERMA',
      $captureGroupId,
      'Entrada real merma producción a MERMA | Captura: ' . $captureUid,
      $userId
    ]);
  }
}

/**
 * Excluye mermas registradas desde Almacén para reportes de Producción.
 * La merma de almacén se guarda en quality_entries para trazabilidad, pero
 * no debe formar parte de cierre de turno, consumo de agua ni total del día.
 */
function production_merma_exclusion_sql(PDO $pdo, string $alias = 'qe'): string {
  $alias = preg_replace('/[^A-Za-z0-9_]/', '', $alias) ?: 'qe';
  $extra = '';

  if (table_exists($pdo, 'warehouse_merma_entries')) {
    $extra .= " AND NOT EXISTS (SELECT 1 FROM warehouse_merma_entries wme WHERE wme.quality_entry_id = {$alias}.id)";
  }

  if (table_exists($pdo, 'quality_concepts')) {
    $extra .= " AND NOT EXISTS (SELECT 1 FROM quality_concepts qc_pm WHERE qc_pm.id = {$alias}.concept_id AND qc_pm.company_id = {$alias}.company_id AND UPPER(qc_pm.name) LIKE '%ALMAC%')";
  }

  return $extra;
}

function get_merma_shift(PDO $pdo, int $companyId, int $locationId, string $date, int $shift): float {
  $mermaExtra = production_merma_exclusion_sql($pdo, 'qe');
  $st = $pdo->prepare("
    SELECT COALESCE(SUM(qe.qty),0)
    FROM quality_entries qe
    WHERE qe.company_id=? AND qe.location_id=? AND qe.work_date=? AND qe.shift=? AND qe.concept_type='MERMA'
      {$mermaExtra}
  ");
  $st->execute([$companyId,$locationId,$date,$shift]);
  return (float)$st->fetchColumn();
}

function get_merma_by_shift(PDO $pdo, int $companyId, int $locationId, string $date): array {
  $mermaExtra = production_merma_exclusion_sql($pdo, 'qe');
  $st = $pdo->prepare("
    SELECT qe.shift, COALESCE(SUM(qe.qty),0) AS qty
    FROM quality_entries qe
    WHERE qe.company_id=? AND qe.location_id=? AND qe.work_date=? AND qe.concept_type='MERMA'
      {$mermaExtra}
    GROUP BY qe.shift
  ");
  $st->execute([$companyId,$locationId,$date]);
  $out = [1=>0.0, 2=>0.0, 3=>0.0];
  foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
    $out[(int)$r['shift']] = (float)$r['qty'];
  }
  return $out;
}

function production_capture_groups_has_rows(PDO $pdo, int $companyId, int $locationId, string $date, ?int $shift = null): bool {
  if (!table_exists($pdo, 'production_capture_groups')) return false;

  if ($shift !== null && in_array($shift, [1,2,3], true)) {
    $st = $pdo->prepare("
      SELECT COUNT(*)
      FROM production_capture_groups
      WHERE company_id=? AND location_id=? AND work_date=? AND shift=?
    ");
    $st->execute([$companyId,$locationId,$date,$shift]);
    return ((int)$st->fetchColumn()) > 0;
  }

  $st = $pdo->prepare("
    SELECT COUNT(*)
    FROM production_capture_groups
    WHERE company_id=? AND location_id=? AND work_date=?
  ");
  $st->execute([$companyId,$locationId,$date]);
  return ((int)$st->fetchColumn()) > 0;
}

function get_capture_rows(PDO $pdo, int $companyId, int $locationId, string $date, ?int $shift = null): array {
  $shiftSql1 = '';
  $shiftSql2 = '';
  $params1 = [$companyId, $locationId, $date];
  $params2 = [$companyId, $locationId, $date];

  if ($shift !== null && in_array($shift, [1,2,3], true)) {
    $shiftSql1 = ' AND cg.shift=? ';
    $shiftSql2 = ' AND pe.shift=? ';
    $params1[] = $shift;
    $params2[] = $shift;
  }

  if (production_capture_groups_has_rows($pdo, $companyId, $locationId, $date, $shift)) {
    $sql = "
      SELECT
        cg.capture_uid,
        cg.entry_time,
        cg.shift,
        cg.qty_total,
        cg.qty_cf,
        cg.qty_cub,
        cg.qty_merma,
        cg.product_id,
        cg.cf_wh_id,
        p.sku,
        p.name AS product_name,
        w.code AS cf_code,
        w.name AS cf_name,
        0 AS is_legacy
      FROM production_capture_groups cg
      JOIN products p ON p.id = cg.product_id
      JOIN warehouses w ON w.id = cg.cf_wh_id
      WHERE cg.company_id=? AND cg.location_id=? AND cg.work_date=?
      {$shiftSql1}
      ORDER BY cg.entry_time DESC, cg.id DESC
      LIMIT 150
    ";
    $st = $pdo->prepare($sql);
    $st->execute($params1);
    return $st->fetchAll(PDO::FETCH_ASSOC);
  }

  $sql = "
    SELECT
      COALESCE(
        NULLIF(pe.capture_uid,''),
        CONCAT('LEG-', pe.company_id, '-', pe.location_id, '-', pe.work_date, '-', pe.shift, '-', pe.product_id, '-', DATE_FORMAT(pe.entry_time,'%Y%m%d%H%i%s'))
      ) AS capture_uid,
      pe.work_date,
      pe.shift,
      pe.product_id,
      pe.entry_time,
      MIN(CASE WHEN w.code IN ('CF1','CF2') THEN pe.warehouse_id END) AS cf_wh_id,
      SUM(pe.qty) AS qty_total,
      SUM(CASE WHEN w.code IN ('CF1','CF2') THEN pe.qty ELSE 0 END) AS qty_cf,
      SUM(CASE WHEN w.code='CUB' THEN pe.qty ELSE 0 END) AS qty_cub
    FROM production_entries pe
    JOIN warehouses w ON w.id = pe.warehouse_id
    WHERE pe.company_id=? AND pe.location_id=? AND pe.work_date=?
    {$shiftSql2}
    GROUP BY
      COALESCE(
        NULLIF(pe.capture_uid,''),
        CONCAT('LEG-', pe.company_id, '-', pe.location_id, '-', pe.work_date, '-', pe.shift, '-', pe.product_id, '-', DATE_FORMAT(pe.entry_time,'%Y%m%d%H%i%s'))
      ),
      pe.work_date, pe.shift, pe.product_id, pe.entry_time
    ORDER BY pe.entry_time DESC
    LIMIT 150
  ";
  $st = $pdo->prepare($sql);
  $st->execute($params2);
  $base = $st->fetchAll(PDO::FETCH_ASSOC);

  if (!$base) return [];

  $out = [];
  $stP = $pdo->prepare("SELECT sku, name FROM products WHERE id=? LIMIT 1");
  $stW = $pdo->prepare("SELECT code, name FROM warehouses WHERE id=? LIMIT 1");

  foreach ($base as $r) {
    $stP->execute([(int)$r['product_id']]);
    $prod = $stP->fetch(PDO::FETCH_ASSOC) ?: ['sku'=>'','name'=>''];

    $cfWhId = (int)($r['cf_wh_id'] ?? 0);
    $cf = ['code'=>'','name'=>''];
    if ($cfWhId > 0) {
      $stW->execute([$cfWhId]);
      $cf = $stW->fetch(PDO::FETCH_ASSOC) ?: ['code'=>'','name'=>''];
    }

    $merma = 0.0;

    if (!empty($r['capture_uid']) && strpos((string)$r['capture_uid'], 'LEG-') !== 0) {
      $mermaExtra = production_merma_exclusion_sql($pdo, 'qe');
      $stM = $pdo->prepare("
        SELECT COALESCE(SUM(qe.qty),0)
        FROM quality_entries qe
        WHERE qe.company_id=? AND qe.location_id=? AND qe.work_date=? AND qe.capture_uid=? AND qe.concept_type='MERMA'
          {$mermaExtra}
      ");
      $stM->execute([$companyId,$locationId,$date,$r['capture_uid']]);
      $merma = (float)$stM->fetchColumn();
    } else {
      $mermaExtra = production_merma_exclusion_sql($pdo, 'qe');
      $stM2 = $pdo->prepare("
        SELECT COALESCE(SUM(qe.qty),0)
        FROM quality_entries qe
        WHERE qe.company_id=?
          AND qe.location_id=?
          AND qe.work_date=?
          AND qe.shift=?
          AND qe.product_id=?
          AND qe.concept_type='MERMA'
          {$mermaExtra}
          AND qe.capture_uid IS NULL
          AND qe.created_at BETWEEN DATE_SUB(?, INTERVAL 2 MINUTE) AND DATE_ADD(?, INTERVAL 2 MINUTE)
      ");
      $stM2->execute([
        $companyId, $locationId, $date,
        (int)$r['shift'], (int)$r['product_id'],
        $r['entry_time'], $r['entry_time']
      ]);
      $merma = (float)$stM2->fetchColumn();
    }

    $out[] = [
      'capture_uid'  => (string)$r['capture_uid'],
      'entry_time'   => (string)$r['entry_time'],
      'shift'        => (int)$r['shift'],
      'qty_total'    => (float)$r['qty_total'],
      'qty_cf'       => (float)$r['qty_cf'],
      'qty_cub'      => (float)$r['qty_cub'],
      'qty_merma'    => (float)$merma,
      'product_id'   => (int)$r['product_id'],
      'cf_wh_id'     => $cfWhId,
      'sku'          => (string)$prod['sku'],
      'product_name' => (string)$prod['name'],
      'cf_code'      => (string)$cf['code'],
      'cf_name'      => (string)$cf['name'],
      'is_legacy'    => 1,
    ];
  }

  return $out;
}

function get_capture_detail(PDO $pdo, int $companyId, int $locationId, string $captureUid): ?array {
  if (strpos($captureUid, 'LEG-') !== 0) {
    $st = $pdo->prepare("
      SELECT
        cg.capture_uid,
        cg.work_date,
        cg.shift,
        cg.product_id,
        cg.entry_time,
        cg.cf_wh_id,
        cg.qty_total,
        cg.qty_cf,
        cg.qty_cub,
        cg.qty_merma,
        p.sku,
        p.name AS product_name,
        w.code AS cf_code,
        w.name AS cf_name,
        0 AS is_legacy
      FROM production_capture_groups cg
      JOIN products p ON p.id = cg.product_id
      JOIN warehouses w ON w.id = cg.cf_wh_id
      WHERE cg.capture_uid=? AND cg.company_id=? AND cg.location_id=?
      LIMIT 1
    ");
    $st->execute([$captureUid,$companyId,$locationId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if ($row) return $row;
  }

  $st = $pdo->prepare("
    SELECT
      COALESCE(
        NULLIF(pe.capture_uid,''),
        CONCAT('LEG-', pe.company_id, '-', pe.location_id, '-', pe.work_date, '-', pe.shift, '-', pe.product_id, '-', DATE_FORMAT(pe.entry_time,'%Y%m%d%H%i%s'))
      ) AS capture_uid,
      pe.work_date,
      pe.shift,
      pe.product_id,
      pe.entry_time,
      MIN(CASE WHEN w.code IN ('CF1','CF2') THEN pe.warehouse_id END) AS cf_wh_id,
      SUM(pe.qty) AS qty_total,
      SUM(CASE WHEN w.code IN ('CF1','CF2') THEN pe.qty ELSE 0 END) AS qty_cf,
      SUM(CASE WHEN w.code='CUB' THEN pe.qty ELSE 0 END) AS qty_cub
    FROM production_entries pe
    JOIN warehouses w ON w.id = pe.warehouse_id
    WHERE pe.company_id=? AND pe.location_id=?
    GROUP BY
      COALESCE(
        NULLIF(pe.capture_uid,''),
        CONCAT('LEG-', pe.company_id, '-', pe.location_id, '-', pe.work_date, '-', pe.shift, '-', pe.product_id, '-', DATE_FORMAT(pe.entry_time,'%Y%m%d%H%i%s'))
      ),
      pe.work_date, pe.shift, pe.product_id, pe.entry_time
    HAVING capture_uid = ?
    LIMIT 1
  ");
  $st->execute([$companyId,$locationId,$captureUid]);
  $row = $st->fetch(PDO::FETCH_ASSOC);
  if (!$row) return null;

  $stP = $pdo->prepare("SELECT sku, name FROM products WHERE id=? LIMIT 1");
  $stP->execute([(int)$row['product_id']]);
  $prod = $stP->fetch(PDO::FETCH_ASSOC) ?: ['sku'=>'','name'=>''];

  $cf = ['code'=>'','name'=>''];
  if ((int)$row['cf_wh_id'] > 0) {
    $stW = $pdo->prepare("SELECT code, name FROM warehouses WHERE id=? LIMIT 1");
    $stW->execute([(int)$row['cf_wh_id']]);
    $cf = $stW->fetch(PDO::FETCH_ASSOC) ?: ['code'=>'','name'=>''];
  }

  $merma = 0.0;
  $mermaExtra = production_merma_exclusion_sql($pdo, 'qe');
  if (strpos($captureUid, 'LEG-') !== 0) {
    $stM = $pdo->prepare("
      SELECT COALESCE(SUM(qe.qty),0)
      FROM quality_entries qe
      WHERE qe.company_id=? AND qe.location_id=? AND qe.work_date=? AND qe.capture_uid=? AND qe.concept_type='MERMA'
        {$mermaExtra}
    ");
    $stM->execute([$companyId,$locationId,$row['work_date'],$captureUid]);
    $merma = (float)$stM->fetchColumn();
  } else {
    $stM2 = $pdo->prepare("
      SELECT COALESCE(SUM(qe.qty),0)
      FROM quality_entries qe
      WHERE qe.company_id=?
        AND qe.location_id=?
        AND qe.work_date=?
        AND qe.shift=?
        AND qe.product_id=?
        AND qe.concept_type='MERMA'
        {$mermaExtra}
        AND qe.capture_uid IS NULL
        AND qe.created_at BETWEEN DATE_SUB(?, INTERVAL 2 MINUTE) AND DATE_ADD(?, INTERVAL 2 MINUTE)
    ");
    $stM2->execute([
      $companyId, $locationId, $row['work_date'],
      (int)$row['shift'], (int)$row['product_id'],
      $row['entry_time'], $row['entry_time']
    ]);
    $merma = (float)$stM2->fetchColumn();
  }

  return [
    'capture_uid'  => (string)$row['capture_uid'],
    'work_date'    => (string)$row['work_date'],
    'shift'        => (int)$row['shift'],
    'product_id'   => (int)$row['product_id'],
    'entry_time'   => (string)$row['entry_time'],
    'cf_wh_id'     => (int)$row['cf_wh_id'],
    'qty_total'    => (float)$row['qty_total'],
    'qty_cf'       => (float)$row['qty_cf'],
    'qty_cub'      => (float)$row['qty_cub'],
    'qty_merma'    => (float)$merma,
    'sku'          => (string)$prod['sku'],
    'product_name' => (string)$prod['name'],
    'cf_code'      => (string)$cf['code'],
    'cf_name'      => (string)$cf['name'],
    'is_legacy'    => 1,
  ];
}

function detect_bar_product(PDO $pdo, int $companyId): ?array {
  $st = $pdo->prepare("
    SELECT id, sku, name, uom
    FROM products
    WHERE company_id=? AND active=1
      AND (
        UPPER(sku) LIKE '%BARRA%150%'
        OR UPPER(name) LIKE '%BARRA%150%'
        OR UPPER(sku) = 'BARRA 150 KILOS'
      )
    ORDER BY id
    LIMIT 1
  ");
  $st->execute([$companyId]);
  $row = $st->fetch(PDO::FETCH_ASSOC);
  return $row ?: null;
}


function build_water_summary(PDO $pdo, int $companyId, int $locationId, string $date): array {
  $bar = detect_bar_product($pdo, $companyId);
  if (!$bar) {
    return [
      'ok' => false,
      'error' => 'No se encontró el producto barra 150 kg.'
    ];
  }

  $barProductId = (int)$bar['id'];
  $litersPerBar = 150.0;
  $reportToBars = 1.0; // 1 barra de 150 kg = 150 litros de agua

  $prodDayId = ensure_prod_day($pdo, $companyId, $locationId, $date, (int)($GLOBALS['u']['id'] ?? 0));
  $st = $pdo->prepare("SELECT target_t1, target_t2, target_t3, target_total FROM production_day WHERE id=? LIMIT 1");
  $st->execute([$prodDayId]);
  $day = $st->fetch(PDO::FETCH_ASSOC) ?: [
    'target_t1' => 400,
    'target_t2' => 400,
    'target_t3' => 400,
    'target_total' => 1200,
  ];

  $st = $pdo->prepare("
    SELECT
      pe.shift,
      COALESCE(SUM(CASE WHEN w.code IN ('CF1','CF2') THEN pe.qty ELSE 0 END),0) AS qty_cf,
      COALESCE(SUM(CASE WHEN w.code='CUB' THEN pe.qty ELSE 0 END),0) AS qty_cub,
      COALESCE(SUM(pe.qty),0) AS qty_ok
    FROM production_entries pe
    JOIN warehouses w ON w.id = pe.warehouse_id
    WHERE pe.company_id=?
      AND pe.location_id=?
      AND pe.work_date=?
      AND pe.product_id=?
    GROUP BY pe.shift
  ");
  $st->execute([$companyId, $locationId, $date, $barProductId]);
  $prodRows = $st->fetchAll(PDO::FETCH_ASSOC);

  $mermaExtra = production_merma_exclusion_sql($pdo, 'qe');
  $st = $pdo->prepare("
    SELECT qe.shift, COALESCE(SUM(qe.qty),0) AS qty_merma
    FROM quality_entries qe
    WHERE qe.company_id=?
      AND qe.location_id=?
      AND qe.work_date=?
      AND qe.product_id=?
      AND qe.concept_type='MERMA'
      {$mermaExtra}
    GROUP BY qe.shift
  ");
  $st->execute([$companyId, $locationId, $date, $barProductId]);
  $mermaRows = $st->fetchAll(PDO::FETCH_ASSOC);

  $targets = [
    1 => (float)($day['target_t1'] ?? 400),
    2 => (float)($day['target_t2'] ?? 400),
    3 => (float)($day['target_t3'] ?? 400),
  ];

  $shifts = [
    1 => ['shift'=>1,'qty_cf'=>0.0,'qty_cub'=>0.0,'qty_merma'=>0.0,'qty_total'=>0.0],
    2 => ['shift'=>2,'qty_cf'=>0.0,'qty_cub'=>0.0,'qty_merma'=>0.0,'qty_total'=>0.0],
    3 => ['shift'=>3,'qty_cf'=>0.0,'qty_cub'=>0.0,'qty_merma'=>0.0,'qty_total'=>0.0],
  ];

  foreach ($prodRows as $r) {
    $shift = (int)($r['shift'] ?? 0);
    if (!isset($shifts[$shift])) continue;
    $shifts[$shift]['qty_cf'] = (float)($r['qty_cf'] ?? 0);
    $shifts[$shift]['qty_cub'] = (float)($r['qty_cub'] ?? 0);
    $shifts[$shift]['qty_total'] = (float)($r['qty_ok'] ?? 0);
  }
  foreach ($mermaRows as $r) {
    $shift = (int)($r['shift'] ?? 0);
    if (!isset($shifts[$shift])) continue;
    $shifts[$shift]['qty_merma'] = (float)($r['qty_merma'] ?? 0);
    $shifts[$shift]['qty_total'] += (float)($r['qty_merma'] ?? 0);
  }

  $summary = [];
  $dayTotals = [
    'qty_cf' => 0.0,
    'qty_cub' => 0.0,
    'qty_merma' => 0.0,
    'qty_total' => 0.0,
    'target_bars' => (float)($day['target_total'] ?? array_sum($targets)),
  ];

  foreach ($shifts as $shift => $row) {
    $producedBars = (float)$row['qty_cf'] + (float)$row['qty_cub'];
    $wastedBars = (float)$row['qty_merma'];
    $reported = $producedBars + $wastedBars;
    $equivBars = $reported * $reportToBars;
    $producedLiters = $producedBars * $litersPerBar;
    $wastedLiters = $wastedBars * $litersPerBar;
    $liters = $producedLiters + $wastedLiters;
    $targetBars = (float)($targets[$shift] ?? 0);
    $targetLiters = $targetBars * $litersPerBar;

    $summary[] = [
      'shift' => $shift,
      'qty_cf' => (float)$row['qty_cf'],
      'qty_cub' => (float)$row['qty_cub'],
      'qty_merma' => (float)$row['qty_merma'],
      'qty_total' => $reported,
      'reported_units' => $reported,
      'equivalent_bars' => $equivBars,
      'produced_bars' => $producedBars,
      'wasted_bars' => $wastedBars,
      'produced_liters' => $producedLiters,
      'wasted_liters' => $wastedLiters,
      'consumed_liters' => $liters,
      'target_bars' => $targetBars,
      'target_liters' => $targetLiters,
      'progress_pct' => ($targetLiters > 0 ? ($liters / $targetLiters) * 100.0 : 0.0),
    ];

    $dayTotals['qty_cf'] += (float)$row['qty_cf'];
    $dayTotals['qty_cub'] += (float)$row['qty_cub'];
    $dayTotals['qty_merma'] += (float)$row['qty_merma'];
    $dayTotals['qty_total'] += $reported;
  }

  $dayTotals['reported_units'] = $dayTotals['qty_total'];
  $dayTotals['produced_bars'] = (float)$dayTotals['qty_cf'] + (float)$dayTotals['qty_cub'];
  $dayTotals['wasted_bars'] = (float)$dayTotals['qty_merma'];
  $dayTotals['equivalent_bars'] = $dayTotals['qty_total'] * $reportToBars;
  $dayTotals['produced_liters'] = $dayTotals['produced_bars'] * $litersPerBar;
  $dayTotals['wasted_liters'] = $dayTotals['wasted_bars'] * $litersPerBar;
  $dayTotals['consumed_liters'] = $dayTotals['produced_liters'] + $dayTotals['wasted_liters'];
  $dayTotals['target_liters'] = $dayTotals['target_bars'] * $litersPerBar;
  $dayTotals['progress_pct'] = $dayTotals['target_liters'] > 0 ? ($dayTotals['consumed_liters'] / $dayTotals['target_liters']) * 100.0 : 0.0;

  return [
    'ok' => true,
    'meta' => [
      'date' => $date,
      'bar_product_id' => $barProductId,
      'bar_sku' => (string)$bar['sku'],
      'bar_name' => (string)$bar['name'],
      'liters_per_bar' => $litersPerBar,
      'report_to_bars' => $reportToBars,
      'simulation_note' => 'Simulación con equivalencia 1 barra de 150 kg = 150 litros de agua. Azul = agua producida, rojo = agua mermada.',
    ],
    'shifts' => $summary,
    'day' => $dayTotals,
  ];
}

function build_shift_report_data(PDO $pdo, int $companyId, int $locationId, array $user, string $date, int $shift, string $tz): array {
  $companyName = '';
  $locationName = '';

  $st = $pdo->prepare("SELECT name FROM companies WHERE id=?");
  $st->execute([$companyId]);
  $companyName = (string)($st->fetchColumn() ?: '');

  $st = $pdo->prepare("SELECT name FROM locations WHERE id=?");
  $st->execute([$locationId]);
  $locationName = (string)($st->fetchColumn() ?: '');

  $bar = detect_bar_product($pdo, $companyId);
  if (!$bar) {
    return [
      'ok' => false,
      'error' => 'No se encontró el producto barra 150 kg.'
    ];
  }

  $barProductId = (int)$bar['id'];

  $st = $pdo->prepare("
    SELECT
      COALESCE(SUM(CASE WHEN w.code IN ('CF1','CF2') THEN pe.qty ELSE 0 END),0) AS bars_cf,
      COALESCE(SUM(CASE WHEN w.code='CUB' THEN pe.qty ELSE 0 END),0) AS bars_cub,
      COALESCE(SUM(pe.qty),0) AS bars_total_ok
    FROM production_entries pe
    JOIN warehouses w ON w.id = pe.warehouse_id
    WHERE pe.company_id=? 
      AND pe.location_id=? 
      AND pe.work_date=? 
      AND pe.shift=? 
      AND pe.product_id=?
  ");
  $st->execute([$companyId, $locationId, $date, $shift, $barProductId]);
  $bars = $st->fetch(PDO::FETCH_ASSOC) ?: [];

  $barsCf      = (float)($bars['bars_cf'] ?? 0);
  $barsCub     = (float)($bars['bars_cub'] ?? 0);
  $barsTotalOk = (float)($bars['bars_total_ok'] ?? 0);

  $mermaExtra = production_merma_exclusion_sql($pdo, 'qe');
  $st = $pdo->prepare("
    SELECT COALESCE(SUM(qe.qty),0)
    FROM quality_entries qe
    WHERE qe.company_id=?
      AND qe.location_id=?
      AND qe.work_date=?
      AND qe.shift=?
      AND qe.product_id=?
      AND qe.concept_type='MERMA'
      {$mermaExtra}
  ");
  $st->execute([$companyId, $locationId, $date, $shift, $barProductId]);
  $barsMerma = (float)$st->fetchColumn();

  $st = $pdo->prepare("
    SELECT
      ce.output_product_id,
      p.sku,
      p.name AS product_name,
      pc.factor,
      COALESCE(SUM(ce.output_qty),0) AS output_qty,
      COALESCE(SUM(ce.bars_used),0) AS bars_used,
      COALESCE(SUM(ce.bars_taken),0) AS bars_taken,
      COALESCE(SUM(ce.bars_leftover),0) AS bars_leftover
    FROM cubing_entries ce
    JOIN products p 
      ON p.id = ce.output_product_id
    LEFT JOIN product_conversions pc
      ON pc.company_id = ce.company_id
     AND pc.from_product_id = ce.input_product_id
     AND pc.to_product_id = ce.output_product_id
     AND pc.active = 1
    WHERE ce.company_id=?
      AND ce.location_id=?
      AND ce.work_date=?
      AND ce.shift=?
      AND ce.input_product_id=?
    GROUP BY ce.output_product_id, p.sku, p.name, pc.factor
    ORDER BY p.sku, p.name
  ");
  $st->execute([$companyId, $locationId, $date, $shift, $barProductId]);
  $conversionRows = $st->fetchAll(PDO::FETCH_ASSOC);

  $convTotals = [
    'output_qty'     => 0.0,
    'bars_used'      => 0.0,
    'bars_taken'     => 0.0,
    'bars_leftover'  => 0.0,
  ];

  foreach ($conversionRows as &$r) {
    $r['factor'] = (float)($r['factor'] ?? 0);
    $r['output_qty'] = (float)($r['output_qty'] ?? 0);
    $r['bars_used'] = (float)($r['bars_used'] ?? 0);
    $r['bars_taken'] = (float)($r['bars_taken'] ?? 0);
    $r['bars_leftover'] = (float)($r['bars_leftover'] ?? 0);

    if ($r['bars_taken'] <= 0 && $r['bars_used'] > 0) {
      $r['bars_taken'] = $r['bars_used'];
    }

    $convTotals['output_qty'] += $r['output_qty'];
    $convTotals['bars_used'] += $r['bars_used'];
    $convTotals['bars_taken'] += $r['bars_taken'];
    $convTotals['bars_leftover'] += $r['bars_leftover'];
  }
  unset($r);

  return [
    'ok' => true,
    'meta' => [
      'company_name'   => $companyName,
      'location_name'  => $locationName,
      'date'           => $date,
      'shift'          => $shift,
      'timezone'       => $tz,
      'user_name'      => (string)($user['full_name'] ?? ''),
      'generated_at'   => tz_now($tz)->format('Y-m-d H:i:s'),
      'bar_product_id' => $barProductId,
      'bar_sku'        => (string)$bar['sku'],
      'bar_name'       => (string)$bar['name'],
    ],
    'summary' => [
      'bars_total_ok'      => $barsTotalOk,
      'bars_cf'            => $barsCf,
      'bars_cub'           => $barsCub,
      'bars_merma'         => $barsMerma,
      'bars_total_turno'   => ($barsTotalOk + $barsMerma),
      'conv_output_qty'    => $convTotals['output_qty'],
      'conv_bars_used'     => $convTotals['bars_used'],
      'conv_bars_taken'    => $convTotals['bars_taken'],
      'conv_bars_leftover' => $convTotals['bars_leftover'],
    ],
    'conversions' => $conversionRows,
  ];
}


function build_day_report_data(PDO $pdo, int $companyId, int $locationId, array $user, string $date, string $tz): array {
  $shifts = [];
  $totals = [
    'bars_total_ok' => 0.0,
    'bars_cf' => 0.0,
    'bars_cub' => 0.0,
    'bars_merma' => 0.0,
    'bars_total_turno' => 0.0,
    'conv_output_qty' => 0.0,
    'conv_bars_used' => 0.0,
    'conv_bars_taken' => 0.0,
    'conv_bars_leftover' => 0.0,
  ];
  $convMap = [];
  $meta = null;

  for ($shift = 1; $shift <= 3; $shift++) {
    $r = build_shift_report_data($pdo, $companyId, $locationId, $user, $date, $shift, $tz);
    if (!$r['ok']) return $r;
    if ($meta === null) $meta = $r['meta'];
    $shifts[] = ['shift' => $shift, 'summary' => $r['summary'], 'conversions' => $r['conversions']];

    foreach ($totals as $k => $_) {
      $totals[$k] += (float)($r['summary'][$k] ?? 0);
    }

    foreach ($r['conversions'] as $cr) {
      $key = (string)($cr['output_product_id'] ?? '') . '|' . (string)($cr['sku'] ?? '') . '|' . (string)($cr['product_name'] ?? '');
      if (!isset($convMap[$key])) {
        $convMap[$key] = [
          'sku' => (string)($cr['sku'] ?? ''),
          'product_name' => (string)($cr['product_name'] ?? ''),
          'factor' => (float)($cr['factor'] ?? 0),
          'output_qty' => 0.0,
          'bars_taken' => 0.0,
          'bars_used' => 0.0,
          'bars_leftover' => 0.0,
        ];
      }
      $convMap[$key]['output_qty'] += (float)($cr['output_qty'] ?? 0);
      $convMap[$key]['bars_taken'] += (float)($cr['bars_taken'] ?? 0);
      $convMap[$key]['bars_used'] += (float)($cr['bars_used'] ?? 0);
      $convMap[$key]['bars_leftover'] += (float)($cr['bars_leftover'] ?? 0);
    }
  }

  if ($meta === null) {
    return ['ok' => false, 'error' => 'No se pudo construir el concentrado del día.'];
  }

  $meta['shift'] = 'DÍA';
  $meta['generated_at'] = tz_now($tz)->format('Y-m-d H:i:s');

  return [
    'ok' => true,
    'meta' => $meta,
    'summary' => $totals,
    'shifts' => $shifts,
    'conversions' => array_values($convMap),
  ];
}

function render_shift_print_html(array $report): string {
  $autoPrint = isset($_GET['auto_print']) && (string)$_GET['auto_print'] === '1';
  $m = $report['meta'];
  $s = $report['summary'];
  $rows = $report['conversions'];

  $convHtml = '';
  if (!$rows) {
    $convHtml = '<tr><td colspan="6" style="text-align:center;">Sin conversiones registradas en este turno.</td></tr>';
  } else {
    foreach ($rows as $r) {
      $convHtml .= '
        <tr>
          <td>'.h($r['sku']).' · '.h($r['product_name']).'</td>
          <td class="num">'.number_format((float)$r['factor'], 3, '.', ',').'</td>
          <td class="num">'.number_format((float)$r['output_qty'], 3, '.', ',').'</td>
          <td class="num">'.number_format((float)$r['bars_taken'], 3, '.', ',').'</td>
          <td class="num">'.number_format((float)$r['bars_used'], 3, '.', ',').'</td>
          <td class="num">'.number_format((float)$r['bars_leftover'], 3, '.', ',').'</td>
        </tr>
      ';
    }
  }

  ob_start();
  ?>
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <title>Reporte de cierre de turno</title>
  <style>
    body{
      font-family: Arial, Helvetica, sans-serif;
      color:#111;
      margin:24px;
      background:#fff;
    }
    .top{
      display:flex;
      justify-content:space-between;
      align-items:flex-start;
      margin-bottom:18px;
    }
    .title{
      text-align:center;
      flex:1;
    }
    .title h1{
      margin:0;
      font-size:22px;
    }
    .title h2{
      margin:4px 0 0;
      font-size:16px;
      font-weight:600;
    }
    .meta{
      width:100%;
      border-collapse:collapse;
      margin:10px 0 16px;
    }
    .meta td{
      border:1px solid #777;
      padding:8px 10px;
      font-size:13px;
    }
    .section-title{
      margin:18px 0 8px;
      font-size:15px;
      font-weight:700;
      background:#f2f2f2;
      border:1px solid #777;
      padding:8px 10px;
    }
    table.tbl{
      width:100%;
      border-collapse:collapse;
      margin-bottom:14px;
    }
    .tbl th,.tbl td{
      border:1px solid #777;
      padding:8px 10px;
      font-size:13px;
    }
    .tbl th{
      background:#f6f6f6;
      text-align:left;
    }
    .num{
      text-align:right;
      white-space:nowrap;
    }
    .signatures{
      margin-top:36px;
      display:grid;
      grid-template-columns: repeat(3, 1fr);
      gap:28px;
    }
    .sig{
      text-align:center;
      font-size:13px;
    }
    .line{
      border-top:1px solid #000;
      margin-top:46px;
      padding-top:6px;
    }
    .obs{
      min-height:70px;
    }
    .printbar{
      margin-bottom:14px;
    }
    @media print{
      .printbar{ display:none; }
      body{ margin:10mm; }
    }
  </style>
</head>
<body>
  <div class="printbar">
    <button onclick="window.print()">Imprimir</button>
    <button onclick="window.close()">Cerrar</button>
  </div>

  <div class="top">
    <div style="width:180px;">
      <strong>Fecha:</strong> <?=h($m['date'])?><br>
      <strong>Turno:</strong> <?=h((string)$m['shift'])?>
    </div>
    <div class="title">
      <h1>FORMATO DE CIERRE DE TURNO</h1>
      <h2>PRODUCCIÓN / RECARGAS</h2>
      <div><?=h($m['company_name'])?> · <?=h($m['location_name'])?></div>
    </div>
    <div style="width:180px; text-align:right;">
      <strong>Zona:</strong> <?=h($m['timezone'])?><br>
      <strong>Generado:</strong> <?=h($m['generated_at'])?>
    </div>
  </div>

  <table class="meta">
    <tr>
      <td><strong>Usuario:</strong> <?=h($m['user_name'])?></td>
      <td><strong>Producto barra:</strong> <?=h($m['bar_sku'])?> · <?=h($m['bar_name'])?></td>
    </tr>
  </table>

  <div class="section-title">Resumen general del turno</div>
  <table class="tbl">
    <thead>
      <tr>
        <th>Concepto</th>
        <th class="num">Cantidad</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Barras OK a almacén directo (CF1 + CF2)</td>
        <td class="num"><?=number_format((float)$s['bars_cf'], 3, '.', ',')?></td>
      </tr>
      <tr>
        <td>Barras enviadas a cubicado (CUB)</td>
        <td class="num"><?=number_format((float)$s['bars_cub'], 3, '.', ',')?></td>
      </tr>
      <tr>
        <td>Merma del turno</td>
        <td class="num"><?=number_format((float)$s['bars_merma'], 3, '.', ',')?></td>
      </tr>
      <tr>
        <td><strong>Total barras producidas del turno</strong></td>
        <td class="num"><strong><?=number_format((float)$s['bars_total_turno'], 3, '.', ',')?></strong></td>
      </tr>
    </tbody>
  </table>

  <div class="section-title">Resumen de conversiones</div>
  <table class="tbl">
    <thead>
      <tr>
        <th>Artículo convertido</th>
        <th class="num">Factor</th>
        <th class="num">Piezas producidas</th>
        <th class="num">Barras tomadas</th>
        <th class="num">Barras convertidas</th>
        <th class="num">Sobrante</th>
      </tr>
    </thead>
    <tbody>
      <?=$convHtml?>
      <tr>
        <td><strong>Totales</strong></td>
        <td></td>
        <td class="num"><strong><?=number_format((float)$s['conv_output_qty'], 3, '.', ',')?></strong></td>
        <td class="num"><strong><?=number_format((float)$s['conv_bars_taken'], 3, '.', ',')?></strong></td>
        <td class="num"><strong><?=number_format((float)$s['conv_bars_used'], 3, '.', ',')?></strong></td>
        <td class="num"><strong><?=number_format((float)$s['conv_bars_leftover'], 3, '.', ',')?></strong></td>
      </tr>
    </tbody>
  </table>

  <div class="section-title">Observaciones</div>
  <table class="tbl">
    <tbody>
      <tr>
        <td class="obs">Merma reportada del turno: <?=number_format((float)$s['bars_merma'], 3, '.', ',')?> barras.</td>
      </tr>
    </tbody>
  </table>

  <div class="signatures">
    <div class="sig">
      <div class="line">Líder</div>
    </div>
    <div class="sig">
      <div class="line">Supervisor</div>
    </div>
    <div class="sig">
      <div class="line">Calidad</div>
    </div>
  </div>
  <?php if ($autoPrint): ?>
  <script>
    window.addEventListener('load', function(){
      setTimeout(function(){ window.print(); }, 350);
    });
  </script>
  <?php endif; ?>
</body>
</html>
  <?php
  return (string)ob_get_clean();
}

function render_day_print_html(array $report): string {
  $autoPrint = isset($_GET['auto_print']) && (string)$_GET['auto_print'] === '1';
  $m = $report['meta'];
  $s = $report['summary'];
  $shiftRows = '';
  foreach (($report['shifts'] ?? []) as $sr) {
    $ts = $sr['summary'];
    $shiftRows .= '<tr><td>Turno '.h((string)$sr['shift']).'</td><td class="num">'.number_format((float)$ts['bars_cf'], 3, '.', ',').'</td><td class="num">'.number_format((float)$ts['bars_cub'], 3, '.', ',').'</td><td class="num">'.number_format((float)$ts['bars_merma'], 3, '.', ',').'</td><td class="num"><strong>'.number_format((float)$ts['bars_total_turno'], 3, '.', ',').'</strong></td></tr>';
  }
  $convRows = '';
  if (empty($report['conversions'])) {
    $convRows = '<tr><td colspan="6" style="text-align:center;">Sin conversiones registradas.</td></tr>';
  } else {
    foreach ($report['conversions'] as $r) {
      $convRows .= '<tr><td>'.h($r['sku']).' · '.h($r['product_name']).'</td><td class="num">'.number_format((float)$r['factor'], 3, '.', ',').'</td><td class="num">'.number_format((float)$r['output_qty'], 3, '.', ',').'</td><td class="num">'.number_format((float)$r['bars_taken'], 3, '.', ',').'</td><td class="num">'.number_format((float)$r['bars_used'], 3, '.', ',').'</td><td class="num">'.number_format((float)$r['bars_leftover'], 3, '.', ',').'</td></tr>';
    }
  }
  ob_start();
  ?>
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <title>Concentrado diario de producción</title>
  <style>
    body{font-family:Arial,Helvetica,sans-serif;color:#111;margin:24px;background:#fff;}
    .top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:18px;}
    .title{text-align:center;flex:1;}.title h1{margin:0;font-size:22px;}.title h2{margin:4px 0 0;font-size:16px;font-weight:600;}
    .meta{width:100%;border-collapse:collapse;margin:10px 0 16px;}.meta td{border:1px solid #777;padding:8px 10px;font-size:13px;}
    .section-title{margin:18px 0 8px;font-size:15px;font-weight:700;background:#f2f2f2;border:1px solid #777;padding:8px 10px;}
    table.tbl{width:100%;border-collapse:collapse;margin-bottom:14px;}.tbl th,.tbl td{border:1px solid #777;padding:8px 10px;font-size:13px;}.tbl th{background:#f6f6f6;text-align:left;}.num{text-align:right;white-space:nowrap;}
    .signatures{margin-top:36px;display:grid;grid-template-columns:repeat(3,1fr);gap:28px;}.sig{text-align:center;font-size:13px;}.line{border-top:1px solid #000;margin-top:46px;padding-top:6px;}.printbar{margin-bottom:14px;}
    @media print{.printbar{display:none;} body{margin:10mm;}}
  </style>
</head>
<body>
  <div class="printbar"><button onclick="window.print()">Imprimir</button> <button onclick="window.close()">Cerrar</button></div>
  <div class="top">
    <div style="width:180px;"><strong>Fecha:</strong> <?=h($m['date'])?></div>
    <div class="title"><h1>CONCENTRADO DIARIO DE PRODUCCIÓN</h1><h2>PRODUCCIÓN / RECARGAS</h2><div><?=h($m['company_name'])?> · <?=h($m['location_name'])?></div></div>
    <div style="width:180px;text-align:right;"><strong>Zona:</strong> <?=h($m['timezone'])?><br><strong>Generado:</strong> <?=h($m['generated_at'])?></div>
  </div>
  <table class="meta"><tr><td><strong>Usuario:</strong> <?=h($m['user_name'])?></td><td><strong>Producto barra:</strong> <?=h($m['bar_sku'])?> · <?=h($m['bar_name'])?></td></tr></table>
  <div class="section-title">Resumen general del día</div>
  <table class="tbl"><tbody>
    <tr><td>Barras OK a almacén directo (CF1 + CF2)</td><td class="num"><?=number_format((float)$s['bars_cf'], 3, '.', ',')?></td></tr>
    <tr><td>Barras enviadas a cubicado (CUB)</td><td class="num"><?=number_format((float)$s['bars_cub'], 3, '.', ',')?></td></tr>
    <tr><td>Merma de producción</td><td class="num"><?=number_format((float)$s['bars_merma'], 3, '.', ',')?></td></tr>
    <tr><td><strong>Total barras del día</strong></td><td class="num"><strong><?=number_format((float)$s['bars_total_turno'], 3, '.', ',')?></strong></td></tr>
    <tr><td>Piezas convertidas</td><td class="num"><?=number_format((float)$s['conv_output_qty'], 3, '.', ',')?></td></tr>
    <tr><td>Barras tomadas conversión</td><td class="num"><?=number_format((float)$s['conv_bars_taken'], 3, '.', ',')?></td></tr>
    <tr><td>Barras convertidas</td><td class="num"><?=number_format((float)$s['conv_bars_used'], 3, '.', ',')?></td></tr>
    <tr><td>Sobrante</td><td class="num"><?=number_format((float)$s['conv_bars_leftover'], 3, '.', ',')?></td></tr>
  </tbody></table>
  <div class="section-title">Detalle por turno</div>
  <table class="tbl"><thead><tr><th>Turno</th><th class="num">CF</th><th class="num">CUB</th><th class="num">Merma prod.</th><th class="num">Total</th></tr></thead><tbody><?=$shiftRows?></tbody></table>
  <div class="section-title">Conversiones consolidadas</div>
  <table class="tbl"><thead><tr><th>Artículo convertido</th><th class="num">Factor</th><th class="num">Piezas producidas</th><th class="num">Barras tomadas</th><th class="num">Barras convertidas</th><th class="num">Sobrante</th></tr></thead><tbody><?=$convRows?></tbody></table>
  <div class="signatures"><div class="sig"><div class="line">Líder</div></div><div class="sig"><div class="line">Supervisor</div></div><div class="sig"><div class="line">Calidad</div></div></div>
  <?php if ($autoPrint): ?><script>window.addEventListener('load', function(){ setTimeout(function(){ window.print(); }, 350); });</script><?php endif; ?>
</body></html>
  <?php
  return (string)ob_get_clean();
}

/* ===================== PDF helpers ===================== */function pdf_escape(string $s): string {
  $s = str_replace("\\", "\\\\", $s);
  $s = str_replace("(", "\\(", $s);
  $s = str_replace(")", "\\)", $s);
  $s = str_replace("\r", "", $s);
  $s = str_replace("\n", " ", $s);
  return $s;
}
function pdf_simple_text(array $lines): string {
  $y = 780;
  $content = "BT\n/F1 12 Tf\n";
  foreach ($lines as $ln) {
    $ln = pdf_escape($ln);
    $content .= "72 $y Td ($ln) Tj\n";
    $y -= 18;
    if ($y < 80) break;
  }
  $content .= "ET\n";
  $len = strlen($content);

  $obj1 = "1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n";
  $obj2 = "2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n";
  $obj3 = "3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj\n";
  $obj4 = "4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj\n";
  $obj5 = "5 0 obj << /Length $len >> stream\n$content\nendstream endobj\n";

  $pdf = "%PDF-1.4\n";
  $offsets = [];
  $offsets[] = strlen($pdf); $pdf .= $obj1;
  $offsets[] = strlen($pdf); $pdf .= $obj2;
  $offsets[] = strlen($pdf); $pdf .= $obj3;
  $offsets[] = strlen($pdf); $pdf .= $obj4;
  $offsets[] = strlen($pdf); $pdf .= $obj5;

  $xrefPos = strlen($pdf);
  $pdf .= "xref\n0 6\n0000000000 65535 f \n";
  foreach ($offsets as $off) {
    $pdf .= str_pad((string)$off, 10, '0', STR_PAD_LEFT) . " 00000 n \n";
  }
  $pdf .= "trailer << /Size 6 /Root 1 0 R >>\nstartxref\n$xrefPos\n%%EOF";
  return $pdf;
}

ensure_capture_schema($pdo);
ensure_signal_detection_schema($pdo);

/* ===================== API ===================== */
if ($action !== '') {
  need_csrf();

  if ($action === 'products') {
    /*
     * Planta Hielo Barra: este módulo SOLO trabaja con BARRA 150 KILOS.
     * No se envían otros productos al front-end para evitar capturas incorrectas.
     */
    $bar = detect_bar_product($pdo, $companyId);
    if (!$bar) {
      jexit(['ok'=>false,'error'=>'No se encontró el producto BARRA 150 KILOS activo para esta empresa.'], 404);
    }
    jexit(['ok'=>true,'rows'=>[$bar]]);
  }

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

  if ($action === 'tz_get') {
    jexit([
      'ok' => true,
      'timezone' => $APP_TZ,
      'options' => $availableTimezones
    ]);
  }

  if ($action === 'tz_save') {
    $d = read_json();
    $tz = (string)($d['timezone'] ?? '');
    if (!isset($availableTimezones[$tz])) {
      jexit(['ok'=>false,'error'=>'Zona horaria inválida'], 400);
    }
    $_SESSION['breport_tz'] = $tz;
    jexit(['ok'=>true, 'timezone'=>$tz]);
  }


  if ($action === 'signal_settings_get') {
    $settings = get_signal_settings($pdo, $companyId, $locationId);
    /* Configuración PLC Hielo Barra: solo Barra 150 Kilos */
    $barCfg = detect_bar_product($pdo, $companyId);
    $productsCfg = $barCfg ? [$barCfg] : [];
    $st = $pdo->prepare("SELECT id, code, name FROM warehouses WHERE company_id=? AND location_id=? AND active=1 AND code IN ('CF1','CF2') ORDER BY code, name");
    $st->execute([$companyId, $locationId]);
    $warehousesCfg = $st->fetchAll(PDO::FETCH_ASSOC);
    $st = $pdo->prepare("SELECT * FROM production_plc_tags WHERE company_id=? AND location_id=? ORDER BY active DESC, tag_code");
    $st->execute([$companyId, $locationId]);
    jexit(['ok'=>true, 'settings'=>$settings, 'products'=>$productsCfg, 'warehouses'=>$warehousesCfg, 'tags'=>$st->fetchAll(PDO::FETCH_ASSOC)]);
  }

  if ($action === 'signal_settings_save') {
    $d = read_json();
    $mode = strtoupper(trim((string)($d['report_mode'] ?? 'MANUAL')));
    if (!in_array($mode, ['MANUAL','AUTOMATIC'], true)) $mode = 'MANUAL';
    $plcEnabled = !empty($d['plc_enabled']) ? 1 : 0;
    $pollSeconds = max(3, min(60, (int)($d['poll_seconds'] ?? 5)));
    $defaultProductId = (int)($d['default_product_id'] ?? 0);
    $barDefault = detect_bar_product($pdo, $companyId);
    if (!$barDefault) {
      jexit(['ok'=>false,'error'=>'No se encontró BARRA 150 KILOS activo. No se puede guardar configuración automática.'], 404);
    }
    // Seguridad: aunque el navegador mande otro artículo, Hielo Barra fuerza Barra 150 Kilos.
    $defaultProductId = (int)$barDefault['id'];
    $defaultCfWhId = (int)($d['default_cf_wh_id'] ?? 0);
    $defaultTotalQty = PRODUCTION_HARVEST_QTY;

    $st = $pdo->prepare(""
      . "UPDATE production_signal_settings "
      . "SET report_mode=?, plc_enabled=?, poll_seconds=?, default_product_id=NULLIF(?,0), default_cf_wh_id=NULLIF(?,0), default_total_qty=?, updated_by=?, updated_at=NOW() "
      . "WHERE company_id=? AND location_id=?"
    );
    $st->execute([$mode, $plcEnabled, $pollSeconds, $defaultProductId, $defaultCfWhId, $defaultTotalQty, (int)$u['id'], $companyId, $locationId]);
    jexit(['ok'=>true]);
  }

  if ($action === 'signal_mode_switch') {
    $d = read_json();
    $targetMode = strtoupper(trim((string)($d['target_mode'] ?? '')));
    $code = trim((string)($d['auth_code'] ?? ''));

    if (!in_array($targetMode, ['MANUAL','AUTOMATIC'], true)) {
      jexit(['ok'=>false,'error'=>'Modo solicitado inválido.'], 400);
    }

    if ($targetMode === 'AUTOMATIC' && $code !== '5321') {
      jexit(['ok'=>false,'error'=>'Código incorrecto. Para cambiar de Manual a Automático usa 5321.'], 403);
    }

    if ($targetMode === 'MANUAL' && $code !== '2304') {
      jexit(['ok'=>false,'error'=>'Código incorrecto. Para cambiar de Automático a Manual usa 2304.'], 403);
    }

    ensure_signal_detection_schema($pdo);

    $plcEnabled = ($targetMode === 'AUTOMATIC') ? 1 : 0;
    $pollSeconds = 5;

    $st = $pdo->prepare("SELECT id, poll_seconds FROM production_signal_settings WHERE company_id=? AND location_id=? LIMIT 1");
    $st->execute([$companyId, $locationId]);
    $settingsRow = $st->fetch(PDO::FETCH_ASSOC);

    if ($settingsRow) {
      $pollSeconds = max(3, min(60, (int)($settingsRow['poll_seconds'] ?? 5)));
      $barSwitch = detect_bar_product($pdo, $companyId);
      $barSwitchId = $barSwitch ? (int)$barSwitch['id'] : 0;
      $upd = $pdo->prepare("
        UPDATE production_signal_settings
        SET report_mode=?, plc_enabled=?, poll_seconds=?,
            default_product_id=IF(? > 0, ?, default_product_id),
            default_total_qty=?, updated_by=?, updated_at=NOW()
        WHERE company_id=? AND location_id=?
      ");
      $upd->execute([$targetMode, $plcEnabled, $pollSeconds, $barSwitchId, $barSwitchId, PRODUCTION_HARVEST_QTY, (int)$u['id'], $companyId, $locationId]);
    } else {
      $barSwitch = detect_bar_product($pdo, $companyId);
      $barSwitchId = $barSwitch ? (int)$barSwitch['id'] : null;
      $ins = $pdo->prepare("
        INSERT INTO production_signal_settings(company_id, location_id, report_mode, plc_enabled, poll_seconds, default_product_id, default_total_qty, updated_by, created_at, updated_at)
        VALUES(?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
      ");
      $ins->execute([$companyId, $locationId, $targetMode, $plcEnabled, $pollSeconds, $barSwitchId, PRODUCTION_HARVEST_QTY, (int)$u['id']]);
    }

    if ($targetMode === 'MANUAL') {
      try {
        $stReset = $pdo->prepare("
          UPDATE production_plc_tags
          SET signal_state=0, status='RESOLVED', handled_at=NOW(), handled_by=?, last_action='RESET', updated_at=NOW()
          WHERE company_id=? AND location_id=? AND active=1 AND signal_state=1
        ");
        $stReset->execute([(int)$u['id'], $companyId, $locationId]);
      } catch (Throwable $e) {}
    }

    jexit([
      'ok'=>true,
      'settings'=>get_signal_settings($pdo, $companyId, $locationId),
      'message'=>($targetMode === 'AUTOMATIC' ? 'Modo automático PLC activado.' : 'Modo manual activado.')
    ]);
  }

  if ($action === 'signal_tag_save') {
    $d = read_json();
    $tagId = (int)($d['id'] ?? 0);
    $tagCode = trim((string)($d['tag_code'] ?? ''));
    $tagName = trim((string)($d['tag_name'] ?? ''));
    $lineName = trim((string)($d['line_name'] ?? ''));
    $active = !empty($d['active']) ? 1 : 0;
    if ($tagCode === '') jexit(['ok'=>false,'error'=>'Captura el código del tag.'], 400);
    if ($tagName === '') $tagName = $tagCode;

    if ($tagId > 0) {
      $st = $pdo->prepare("UPDATE production_plc_tags SET tag_code=?, tag_name=?, line_name=?, active=?, status=IF(?=1 AND status='DISABLED','IDLE',status), updated_at=NOW() WHERE id=? AND company_id=? AND location_id=?");
      $st->execute([$tagCode, $tagName, $lineName, $active, $active, $tagId, $companyId, $locationId]);
    } else {
      $st = $pdo->prepare(""
        . "INSERT INTO production_plc_tags(company_id, location_id, tag_code, tag_name, line_name, active, status, signal_state, created_at) "
        . "VALUES(?, ?, ?, ?, ?, ?, 'IDLE', 0, NOW()) "
        . "ON DUPLICATE KEY UPDATE tag_name=VALUES(tag_name), line_name=VALUES(line_name), active=VALUES(active), updated_at=NOW()"
      );
      $st->execute([$companyId, $locationId, $tagCode, $tagName, $lineName, $active]);
    }
    jexit(['ok'=>true]);
  }

  if ($action === 'signal_pending') {
    $settings = get_signal_settings($pdo, $companyId, $locationId);
    if ((string)($settings['report_mode'] ?? 'MANUAL') !== 'AUTOMATIC' || (int)($settings['plc_enabled'] ?? 0) !== 1) {
      jexit(['ok'=>true, 'pending'=>false, 'settings'=>$settings]);
    }

    $pdo->beginTransaction();
    try {
      $st = $pdo->prepare(""
        . "SELECT * FROM production_plc_tags "
        . "WHERE company_id=? AND location_id=? AND active=1 AND signal_state=1 AND status IN ('IDLE','RESOLVED') "
        . "ORDER BY COALESCE(last_value_at, updated_at, created_at), id LIMIT 1 FOR UPDATE"
      );
      $st->execute([$companyId, $locationId]);
      $tag = $st->fetch(PDO::FETCH_ASSOC);
      if (!$tag) {
        $pdo->commit();
        jexit(['ok'=>true, 'pending'=>false, 'settings'=>$settings]);
      }
      $up = $pdo->prepare("UPDATE production_plc_tags SET status='IN_PROCESS', opened_at=NOW(), opened_by=?, updated_at=NOW() WHERE id=?");
      $up->execute([(int)$u['id'], (int)$tag['id']]);
      $pdo->commit();
      $tag['status'] = 'IN_PROCESS';
      jexit(['ok'=>true, 'pending'=>true, 'tag'=>$tag, 'settings'=>$settings]);
    } catch (Throwable $e) {
      if ($pdo->inTransaction()) $pdo->rollBack();
      jexit(['ok'=>false, 'error'=>'Error leyendo señal PLC: '.$e->getMessage()], 500);
    }
  }

  if ($action === 'signal_maintenance') {
    $d = read_json();
    $tagId = (int)($d['tag_id'] ?? 0);
    $date = (string)($d['date'] ?? tz_today($APP_TZ));
    $notes = trim((string)($d['notes'] ?? ''));
    if ($tagId <= 0) jexit(['ok'=>false,'error'=>'Tag inválido.'], 400);
    $running = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    if (!$running) jexit(['ok'=>false,'error'=>'Debes iniciar turno antes de registrar mantenimiento.'], 409);

    $st = $pdo->prepare("SELECT id, tag_code FROM production_plc_tags WHERE id=? AND company_id=? AND location_id=? LIMIT 1");
    $st->execute([$tagId, $companyId, $locationId]);
    $tag = $st->fetch(PDO::FETCH_ASSOC);
    if (!$tag) jexit(['ok'=>false,'error'=>'Tag no encontrado.'], 404);

    $pdo->beginTransaction();
    try {
      $ins = $pdo->prepare(""
        . "INSERT INTO production_downtime_events(company_id, location_id, work_date, shift, tag_id, tag_code, event_time, reason, notes, created_by, created_at) "
        . "VALUES(?, ?, ?, ?, ?, ?, NOW(), 'MANTENIMIENTO', ?, ?, NOW())"
      );
      $ins->execute([$companyId, $locationId, $date, (int)$running['shift'], $tagId, (string)$tag['tag_code'], ($notes !== '' ? mb_substr($notes,0,255) : null), (int)$u['id']]);
      reset_signal_tag($pdo, $companyId, $locationId, $tagId, (int)$u['id'], 'MAINTENANCE');
      $pdo->commit();
      jexit(['ok'=>true]);
    } catch (Throwable $e) {
      if ($pdo->inTransaction()) $pdo->rollBack();
      jexit(['ok'=>false,'error'=>'Error registrando mantenimiento: '.$e->getMessage()], 500);
    }
  }

  if ($action === 'signal_reset') {
    $d = read_json();
    $tagId = (int)($d['tag_id'] ?? 0);
    reset_signal_tag($pdo, $companyId, $locationId, $tagId, (int)$u['id'], 'RESET');
    jexit(['ok'=>true]);
  }

  if ($action === 'shift_status') {
    $date = (string)($_GET['date'] ?? tz_today($APP_TZ));
    $row = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    jexit(['ok'=>true,'running'=>$row]);
  }

  if ($action === 'shift_start') {
    $d = read_json();
    $date  = (string)($d['date'] ?? tz_today($APP_TZ));
    $shift = (int)($d['shift'] ?? 0);
    if (!in_array($shift,[1,2,3],true)) jexit(['ok'=>false,'error'=>'Turno inválido'], 400);

    $row = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    if ($row) jexit(['ok'=>true,'running'=>$row,'already'=>true]);

    ensure_prod_day($pdo, $companyId, $locationId, $date, (int)$u['id']);

    $st = $pdo->prepare("
      INSERT INTO shift_sessions(company_id,location_id,user_id,work_date,shift,start_at,status,last_heartbeat_at)
      VALUES(?,?,?,?,?,NOW(),'RUNNING',NOW())
    ");
    $st->execute([$companyId,$locationId,(int)$u['id'],$date,$shift]);

    $row = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    jexit(['ok'=>true,'running'=>$row,'already'=>false]);
  }


  if ($action === 'water_summary') {
    $date = (string)($_GET['date'] ?? tz_today($APP_TZ));
    $data = build_water_summary($pdo, $companyId, $locationId, $date);
    if (!$data['ok']) jexit($data, 409);
    jexit($data);
  }

  if ($action === 'shift_summary') {
    $d = read_json();
    $date = (string)($d['date'] ?? tz_today($APP_TZ));

    $running = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    $requestedShift = (int)($d['shift'] ?? 0);
    $shift = in_array($requestedShift, [1,2,3], true)
      ? $requestedShift
      : ($running ? (int)$running['shift'] : detect_shift_by_time($date, $APP_TZ));

    $report = build_shift_report_data($pdo, $companyId, $locationId, $u, $date, $shift, $APP_TZ);
    if (!$report['ok']) jexit($report, 409);

    jexit([
      'ok'   => true,
      'shift'=> $shift,
      'is_running' => ($running && (int)$running['shift'] === $shift),
      'running' => $running,
      'meta' => $report['meta'],
      'summary' => $report['summary'],
      'conversions' => $report['conversions']
    ]);
  }

  if ($action === 'shift_summary_pdf') {
    $date = (string)($_GET['date'] ?? tz_today($APP_TZ));

    $running = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    $requestedShift = (int)($_GET['shift'] ?? 0);
    $shift = in_array($requestedShift, [1,2,3], true)
      ? $requestedShift
      : ($running ? (int)$running['shift'] : detect_shift_by_time($date, $APP_TZ));

    $report = build_shift_report_data($pdo, $companyId, $locationId, $u, $date, $shift, $APP_TZ);
    if (!$report['ok']) {
      http_response_code(409);
      header('Content-Type: text/plain; charset=utf-8');
      echo $report['error'] ?? 'No se pudo generar el reporte.';
      exit;
    }

    $m = $report['meta'];
    $s = $report['summary'];

    $lines = [
      "BReport - Cierre de Turno",
      "Empresa: ".$m['company_name'],
      "Planta/Localidad: ".$m['location_name'],
      "Fecha: ".$m['date'],
      "Turno: ".$m['shift'],
      "Zona horaria: ".$m['timezone'],
      "Usuario: ".$m['user_name'],
      "----------------------------------------",
      "Barras directas CF: ".number_format($s['bars_cf'], 3, '.', ''),
      "Barras a cubicado: ".number_format($s['bars_cub'], 3, '.', ''),
      "Merma: ".number_format($s['bars_merma'], 3, '.', ''),
      "Total barras turno: ".number_format($s['bars_total_turno'], 3, '.', ''),
      "----------------------------------------",
      "Conversiones:",
    ];

    foreach ($report['conversions'] as $r) {
      $lines[] = $r['sku']." | Pzas: ".number_format((float)$r['output_qty'], 3, '.', '')." | Tomadas: ".number_format((float)$r['bars_taken'], 3, '.', '')." | Convertidas: ".number_format((float)$r['bars_used'], 3, '.', '')." | Sobrante: ".number_format((float)$r['bars_leftover'], 3, '.', '');
    }

    $lines[] = "----------------------------------------";
    $lines[] = "Firma Lider: __________________________";
    $lines[] = "Firma Supervisor: _____________________";
    $lines[] = "Firma Calidad: ________________________";
    $lines[] = "Generado: ".$m['generated_at'];

    $pdf = pdf_simple_text($lines);

    header('Content-Type: application/pdf');
    header('Content-Disposition: attachment; filename="BReport_Cierre_'.$date.'_T'.$shift.'.pdf"');
    header('Content-Length: '.strlen($pdf));
    echo $pdf;
    exit;
  }


  if ($action === 'day_summary_pdf') {
    $date = (string)($_GET['date'] ?? tz_today($APP_TZ));
    $report = build_day_report_data($pdo, $companyId, $locationId, $u, $date, $APP_TZ);
    if (!$report['ok']) {
      http_response_code(409);
      header('Content-Type: text/plain; charset=utf-8');
      echo $report['error'] ?? 'No se pudo generar el concentrado del día.';
      exit;
    }

    $m = $report['meta'];
    $s = $report['summary'];
    $lines = [
      "BReport - Concentrado Diario de Produccion",
      "Empresa: ".$m['company_name'],
      "Planta/Localidad: ".$m['location_name'],
      "Fecha: ".$m['date'],
      "Zona horaria: ".$m['timezone'],
      "Usuario: ".$m['user_name'],
      "----------------------------------------",
      "RESUMEN DEL DIA",
      "Barras directas CF: ".number_format($s['bars_cf'], 3, '.', ''),
      "Barras a cubicado: ".number_format($s['bars_cub'], 3, '.', ''),
      "Merma de produccion: ".number_format($s['bars_merma'], 3, '.', ''),
      "Total barras del dia: ".number_format($s['bars_total_turno'], 3, '.', ''),
      "Piezas convertidas: ".number_format($s['conv_output_qty'], 3, '.', ''),
      "Barras tomadas conversion: ".number_format($s['conv_bars_taken'], 3, '.', ''),
      "Barras convertidas: ".number_format($s['conv_bars_used'], 3, '.', ''),
      "Sobrante: ".number_format($s['conv_bars_leftover'], 3, '.', ''),
      "----------------------------------------",
      "Detalle por turno:",
    ];

    foreach ($report['shifts'] as $sr) {
      $ts = $sr['summary'];
      $lines[] = "Turno ".$sr['shift']." | CF: ".number_format($ts['bars_cf'], 3, '.', '')." | CUB: ".number_format($ts['bars_cub'], 3, '.', '')." | Merma prod: ".number_format($ts['bars_merma'], 3, '.', '')." | Total: ".number_format($ts['bars_total_turno'], 3, '.', '');
    }

    $lines[] = "----------------------------------------";
    $lines[] = "Conversiones consolidadas:";
    foreach ($report['conversions'] as $r) {
      $lines[] = $r['sku']." | Pzas: ".number_format((float)$r['output_qty'], 3, '.', '')." | Tomadas: ".number_format((float)$r['bars_taken'], 3, '.', '')." | Convertidas: ".number_format((float)$r['bars_used'], 3, '.', '')." | Sobrante: ".number_format((float)$r['bars_leftover'], 3, '.', '');
    }
    $lines[] = "----------------------------------------";
    $lines[] = "Firma Lider: __________________________";
    $lines[] = "Firma Supervisor: _____________________";
    $lines[] = "Firma Calidad: ________________________";
    $lines[] = "Generado: ".$m['generated_at'];

    $pdf = pdf_simple_text($lines);
    header('Content-Type: application/pdf');
    header('Content-Disposition: attachment; filename="BReport_Concentrado_Dia_'.$date.'.pdf"');
    header('Content-Length: '.strlen($pdf));
    echo $pdf;
    exit;
  }

  if ($action === 'shift_print_report') {
    $date = (string)($_GET['date'] ?? tz_today($APP_TZ));

    $running = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    $requestedShift = (int)($_GET['shift'] ?? 0);
    $shift = in_array($requestedShift, [1,2,3], true)
      ? $requestedShift
      : ($running ? (int)$running['shift'] : detect_shift_by_time($date, $APP_TZ));

    $report = build_shift_report_data($pdo, $companyId, $locationId, $u, $date, $shift, $APP_TZ);
    if (!$report['ok']) {
      http_response_code(409);
      header('Content-Type: text/plain; charset=utf-8');
      echo $report['error'] ?? 'No se pudo generar el reporte.';
      exit;
    }

    header('Content-Type: text/html; charset=utf-8');
    echo render_shift_print_html($report);
    exit;
  }

  if ($action === 'day_print_report') {
    $date = (string)($_GET['date'] ?? tz_today($APP_TZ));
    $report = build_day_report_data($pdo, $companyId, $locationId, $u, $date, $APP_TZ);
    if (!$report['ok']) {
      http_response_code(409);
      header('Content-Type: text/plain; charset=utf-8');
      echo $report['error'] ?? 'No se pudo generar el concentrado del día.';
      exit;
    }
    header('Content-Type: text/html; charset=utf-8');
    echo render_day_print_html($report);
    exit;
  }
  if ($action === 'shift_end') {
    $d = read_json();
    $date = (string)($d['date'] ?? tz_today($APP_TZ));

    $row = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    if (!$row) jexit(['ok'=>false,'error'=>'No hay turno activo'], 409);

    $st = $pdo->prepare("
      UPDATE shift_sessions
      SET status='ENDED', end_at=NOW(), last_heartbeat_at=NOW()
      WHERE id=? AND company_id=? AND location_id=? AND user_id=? AND status='RUNNING'
    ");
    $st->execute([(int)$row['id'], $companyId, $locationId, (int)$u['id']]);

    jexit(['ok'=>true]);
  }

  if ($action === 'shift_heartbeat') {
    $d = read_json();
    $date = (string)($d['date'] ?? tz_today($APP_TZ));
    $row = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    if (!$row) jexit(['ok'=>true,'skipped'=>true]);

    $st = $pdo->prepare("
      UPDATE shift_sessions
      SET last_heartbeat_at=NOW()
      WHERE id=? AND company_id=? AND location_id=? AND user_id=? AND status='RUNNING'
    ");
    $st->execute([(int)$row['id'], $companyId, $locationId, (int)$u['id']]);

    jexit(['ok'=>true]);
  }

  if ($action === 'day_load') {
    $date = (string)($_GET['date'] ?? tz_today($APP_TZ));
    $prodDayId = ensure_prod_day($pdo, $companyId, $locationId, $date, (int)$u['id']);

    // Recalcula al cargar para corregir contadores históricos ya guardados
    // cuando el total del día quedó inflado por detalle duplicado.
    recalc_day($pdo, $companyId, $locationId, $date);

    $st = $pdo->prepare("
      SELECT id, prod_date, antenna_total_qty, total_prod_qty, diff_qty, warn_flag, locked,
             target_t1, target_t2, target_t3, target_total
      FROM production_day WHERE id=?
    ");
    $st->execute([$prodDayId]);
    $day = $st->fetch(PDO::FETCH_ASSOC);

    $running = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    $displayShift = $running ? (int)$running['shift'] : detect_shift_by_time($date, $APP_TZ);

    $reqShift = isset($_GET['shift']) ? (int)$_GET['shift'] : 0;
    $selectedShift = in_array($reqShift, [1,2,3], true) ? $reqShift : $displayShift;

    // Contadores por turno: CF + CUB + MERMA real capturada.
    // Si existen grupos de captura, se usa qty_total de production_capture_groups
    // para evitar doble conteo por registros detalle.
    $byShift = get_production_shift_totals($pdo, $companyId, $locationId, $date);

    if (production_capture_groups_has_rows($pdo, $companyId, $locationId, $date, $selectedShift)) {
      $st = $pdo->prepare("
        SELECT w.code, w.name, COALESCE(SUM(cg.qty_cf),0) AS qty_ok
        FROM production_capture_groups cg
        JOIN warehouses w ON w.id = cg.cf_wh_id
        WHERE cg.company_id=? AND cg.location_id=? AND cg.work_date=? AND cg.shift=?
          AND cg.qty_cf > 0
        GROUP BY w.id, w.code, w.name

        UNION ALL

        SELECT w.code, w.name, COALESCE(SUM(cg.qty_cub),0) AS qty_ok
        FROM production_capture_groups cg
        JOIN warehouses w
          ON w.company_id = cg.company_id
         AND w.location_id = cg.location_id
         AND UPPER(w.code) = 'CUB'
         AND w.active = 1
        WHERE cg.company_id=? AND cg.location_id=? AND cg.work_date=? AND cg.shift=?
          AND cg.qty_cub > 0
        GROUP BY w.id, w.code, w.name
      ");
      $st->execute([$companyId,$locationId,$date,$selectedShift,$companyId,$locationId,$date,$selectedShift]);
      $byWh = $st->fetchAll(PDO::FETCH_ASSOC);
    } else {
      $st = $pdo->prepare("
        SELECT w.code, w.name, COALESCE(SUM(e.qty),0) AS qty_ok
        FROM production_entries e
        JOIN warehouses w ON w.id = e.warehouse_id
        WHERE e.company_id=? AND e.location_id=? AND e.work_date=? AND e.shift=?
        GROUP BY w.id, w.code, w.name
        ORDER BY FIELD(w.code,'CF1','CF2','CUB','CF3'), w.name
      ");
      $st->execute([$companyId,$locationId,$date,$selectedShift]);
      $byWh = $st->fetchAll(PDO::FETCH_ASSOC);
    }

    usort($byWh, function(array $a, array $b): int {
      $order = ['CF1'=>1, 'CF2'=>2, 'CUB'=>3, 'CF3'=>4];
      $pa = $order[strtoupper((string)($a['code'] ?? ''))] ?? 99;
      $pb = $order[strtoupper((string)($b['code'] ?? ''))] ?? 99;
      if ($pa !== $pb) return $pa <=> $pb;
      return strcmp((string)($a['name'] ?? ''), (string)($b['name'] ?? ''));
    });

    $captures = get_capture_rows($pdo, $companyId, $locationId, $date, $selectedShift);

    $mermaByShift = get_merma_by_shift($pdo, $companyId, $locationId, $date);
    $mermaCurrent = (float)($mermaByShift[$selectedShift] ?? 0);

    jexit([
      'ok'=>true,
      'day'=>$day,
      'by_shift'=>$byShift,
      'by_wh'=>$byWh,
      'captures'=>$captures,
      'running'=>$running,
      'merma_by_shift'=>$mermaByShift,
      'merma_current'=>$mermaCurrent,
      'display_shift'=>$displayShift,
      'selected_shift'=>$selectedShift,
      'timezone'=>$APP_TZ
    ]);
  }

  if ($action === 'capture_get') {
    $uid = (string)($_GET['capture_uid'] ?? '');
    if ($uid === '') jexit(['ok'=>false,'error'=>'capture_uid requerido'], 400);

    $row = get_capture_detail($pdo, $companyId, $locationId, $uid);
    if (!$row) jexit(['ok'=>false,'error'=>'Captura no encontrada'], 404);

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

  if ($action === 'targets_save') {
    $d = read_json();
    $date = (string)($d['date'] ?? tz_today($APP_TZ));
    $t1 = (float)($d['target_t1'] ?? 400);
    $t2 = (float)($d['target_t2'] ?? 400);
    $t3 = (float)($d['target_t3'] ?? 400);
    $tt = (float)($d['target_total'] ?? ($t1+$t2+$t3));

    $prodDayId = ensure_prod_day($pdo, $companyId, $locationId, $date, (int)$u['id']);
    $st = $pdo->prepare("SELECT locked FROM production_day WHERE id=?");
    $st->execute([$prodDayId]);
    if ((int)$st->fetchColumn() === 1) jexit(['ok'=>false,'error'=>'Día bloqueado.'], 409);

    $st = $pdo->prepare("UPDATE production_day SET target_t1=?, target_t2=?, target_t3=?, target_total=? WHERE id=?");
    $st->execute([$t1,$t2,$t3,$tt,$prodDayId]);

    jexit(['ok'=>true]);
  }

  if ($action === 'antenna_save') {
    $d = read_json();
    $date = (string)($d['date'] ?? tz_today($APP_TZ));
    $val  = (float)($d['antenna_total_qty'] ?? 0);

    $prodDayId = ensure_prod_day($pdo, $companyId, $locationId, $date, (int)$u['id']);
    $st = $pdo->prepare("SELECT locked FROM production_day WHERE id=?");
    $st->execute([$prodDayId]);
    if ((int)$st->fetchColumn() === 1) jexit(['ok'=>false,'error'=>'Día bloqueado.'], 409);

    $st = $pdo->prepare("UPDATE production_day SET antenna_total_qty=? WHERE id=?");
    $st->execute([$val, $prodDayId]);

    recalc_day($pdo, $companyId, $locationId, $date);
    jexit(['ok'=>true]);
  }

  if ($action === 'day_lock') {
    $d = read_json();
    $date = (string)($d['date'] ?? tz_today($APP_TZ));
    $lock = (int)($d['lock'] ?? 1) ? 1 : 0;

    if (!has_role('admin') && !has_role('plant_admin')) {
      jexit(['ok'=>false,'error'=>'No autorizado'], 403);
    }

    $prodDayId = ensure_prod_day($pdo, $companyId, $locationId, $date, (int)$u['id']);
    $st = $pdo->prepare("
      UPDATE production_day
      SET locked=?, locked_at=IF(?,NOW(),NULL), locked_by=IF(?, ?, NULL)
      WHERE id=?
    ");
    $st->execute([$lock, $lock, $lock, (int)$u['id'], $prodDayId]);

    jexit(['ok'=>true]);
  }

  if ($action === 'entry_add_bundle') {
    $d = read_json();
    $date      = (string)($d['date'] ?? tz_today($APP_TZ));
    $productId = (int)($d['product_id'] ?? 0);
    $entryTime = normalize_dt_local((string)($d['entry_time'] ?? ''), $APP_TZ);

    $total   = (float)($d['total'] ?? 0);
    $cf_wh_id= (int)($d['cf_wh_id'] ?? 0);
    $q_cf    = (float)($d['q_cf'] ?? 0);
    $q_cub   = (float)($d['q_cub'] ?? 0);
    $q_mer   = (float)($d['q_merma'] ?? 0);
    $plcTagId = (int)($d['plc_tag_id'] ?? 0);

    $barEntry = detect_bar_product($pdo, $companyId);
    if (!$barEntry) jexit(['ok'=>false,'error'=>'No se encontró BARRA 150 KILOS activo.'], 404);
    // Seguridad de backend: este módulo solo permite Barra 150 kg.
    $productId = (int)$barEntry['id'];
    if ($productId<=0) jexit(['ok'=>false,'error'=>'Producto inválido'], 400);
    if ($total<=0) jexit(['ok'=>false,'error'=>'Total inválido'], 400);
    if (abs($total - PRODUCTION_HARVEST_QTY) > 0.00001) {
      jexit(['ok'=>false,'error'=>'La cantidad por cosecha es fija: 10. No se permite reportar más de una cosecha en una sola captura.'], 409);
    }
    if ($cf_wh_id<=0) jexit(['ok'=>false,'error'=>'Destino CF inválido'], 400);
    if ($q_cf<0 || $q_cub<0 || $q_mer<0) jexit(['ok'=>false,'error'=>'No se permiten negativos'], 400);

    $sum = $q_cf + $q_cub + $q_mer;
    if (abs($sum - $total) > 0.00001) {
      jexit(['ok'=>false,'error'=>"La suma (CF + CUB + Merma) debe ser igual al Total. Total=$total, Suma=$sum"], 409);
    }

    $running = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
    if (!$running) jexit(['ok'=>false,'error'=>'Debes iniciar turno antes de reportar producción.'], 409);
    $shift = (int)$running['shift'];

    $prodDayId = ensure_prod_day($pdo, $companyId, $locationId, $date, (int)$u['id']);
    $st = $pdo->prepare("SELECT locked FROM production_day WHERE id=?");
    $st->execute([$prodDayId]);
    if ((int)$st->fetchColumn() === 1) jexit(['ok'=>false,'error'=>'Día bloqueado.'], 409);

    $st = $pdo->prepare("
      SELECT code
      FROM warehouses
      WHERE id=? AND company_id=? AND location_id=? AND active=1
      LIMIT 1
    ");
    $st->execute([$cf_wh_id,$companyId,$locationId]);
    $cfCode = (string)($st->fetchColumn() ?: '');
    if (!in_array($cfCode, ['CF1','CF2'], true)) {
      jexit(['ok'=>false,'error'=>'Destino CF debe ser CF1 o CF2'], 409);
    }

    $whCUB = wh_id_by_code($pdo,$companyId,$locationId,'CUB');
    if ($q_cub>0 && $whCUB<=0) jexit(['ok'=>false,'error'=>'Falta warehouse CUB en este alcance'], 500);

    $st = $pdo->prepare("
      SELECT id
      FROM quality_concepts
      WHERE company_id=? AND concept_type='MERMA' AND active=1
      ORDER BY sort_order, id
      LIMIT 1
    ");
    $st->execute([$companyId]);
    $mermaConceptId = (int)($st->fetchColumn() ?: 1);

    $captureUid = 'CAP-'.date('YmdHis').'-'.bin2hex(random_bytes(4));

    $pdo->beginTransaction();
    try {
      $stCG = $pdo->prepare("
        INSERT INTO production_capture_groups(
          capture_uid, company_id, location_id, work_date, shift, product_id, entry_time, cf_wh_id,
          qty_total, qty_cf, qty_cub, qty_merma, created_by, created_at
        )
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())
      ");
      $stCG->execute([
        $captureUid, $companyId, $locationId, $date, $shift, $productId, $entryTime, $cf_wh_id,
        $total, $q_cf, $q_cub, $q_mer, (int)$u['id']
      ]);
      $captureGroupId = (int)$pdo->lastInsertId();

      $ins = $pdo->prepare("
        INSERT INTO production_entries(
          company_id,location_id,work_date,shift,product_id,warehouse_id,qty,entry_time,capture_uid,created_by
        ) VALUES(?,?,?,?,?,?,?,?,?,?)
      ");
      if ($q_cf>0) $ins->execute([$companyId,$locationId,$date,$shift,$productId,$cf_wh_id,$q_cf,$entryTime,$captureUid,(int)$u['id']]);
      if ($q_cub>0) $ins->execute([$companyId,$locationId,$date,$shift,$productId,$whCUB,$q_cub,$entryTime,$captureUid,(int)$u['id']]);

      insert_production_inventory_moves_for_capture(
        $pdo, $companyId, $locationId, $date, $productId, $cf_wh_id, $whCUB,
        $q_cf, $q_cub, $q_mer, $captureUid, $captureGroupId, (int)$u['id']
      );

      if ($q_mer>0) {
        $stQ = $pdo->prepare("
          INSERT INTO quality_entries(
            company_id,location_id,work_date,shift,product_id,concept_type,concept_id,qty,notes,capture_uid,created_by,created_at
          ) VALUES(?,?,?,?,?,'MERMA',?,?,NULL,?,?,NOW())
        ");
        $stQ->execute([$companyId,$locationId,$date,$shift,$productId,$mermaConceptId,$q_mer,$captureUid,(int)$u['id']]);
      }

      if ($plcTagId > 0) {
        reset_signal_tag($pdo, $companyId, $locationId, $plcTagId, (int)$u['id'], 'PRODUCTION');
      }

      $pdo->commit();
    } catch (Throwable $e) {
      $pdo->rollBack();
      jexit(['ok'=>false,'error'=>'Error guardando captura: '.$e->getMessage()], 500);
    }

    recalc_day($pdo, $companyId, $locationId, $date);
    jexit(['ok'=>true, 'capture_uid'=>$captureUid]);
  }

  if ($action === 'capture_update') {
    $d = read_json();
    $captureUid = (string)($d['capture_uid'] ?? '');
    $date       = (string)($d['date'] ?? '');
    $productId  = (int)($d['product_id'] ?? 0);
    $entryTime  = normalize_dt_local((string)($d['entry_time'] ?? ''), $APP_TZ);
    $total      = (float)($d['total'] ?? 0);
    $cf_wh_id   = (int)($d['cf_wh_id'] ?? 0);
    $q_cf       = (float)($d['q_cf'] ?? 0);
    $q_cub      = (float)($d['q_cub'] ?? 0);
    $q_mer      = (float)($d['q_merma'] ?? 0);

    if ($captureUid === '') jexit(['ok'=>false,'error'=>'capture_uid requerido'], 400);
    if ($date === '') jexit(['ok'=>false,'error'=>'Fecha requerida'], 400);
    $barEntry = detect_bar_product($pdo, $companyId);
    if (!$barEntry) jexit(['ok'=>false,'error'=>'No se encontró BARRA 150 KILOS activo.'], 404);
    // Seguridad de backend: este módulo solo permite Barra 150 kg.
    $productId = (int)$barEntry['id'];
    if ($productId<=0) jexit(['ok'=>false,'error'=>'Producto inválido'], 400);
    if ($cf_wh_id<=0) jexit(['ok'=>false,'error'=>'Destino CF inválido'], 400);
    if ($q_cf<0 || $q_cub<0 || $q_mer<0) jexit(['ok'=>false,'error'=>'No se permiten negativos'], 400);
    if (abs($total - PRODUCTION_HARVEST_QTY) > 0.00001) {
      jexit(['ok'=>false,'error'=>'La cantidad por cosecha es fija: 10. No se permite editar una captura a más de una cosecha.'], 409);
    }

    $sum = $q_cf + $q_cub + $q_mer;
    if (abs($sum - $total) > 0.00001) {
      jexit(['ok'=>false,'error'=>'La suma de cantidades debe coincidir con el total'], 409);
    }

    $st = $pdo->prepare("SELECT locked FROM production_day WHERE company_id=? AND location_id=? AND prod_date=? LIMIT 1");
    $st->execute([$companyId,$locationId,$date]);
    if ((int)$st->fetchColumn() === 1) jexit(['ok'=>false,'error'=>'Día bloqueado.'], 409);

    $st = $pdo->prepare("
      SELECT code
      FROM warehouses
      WHERE id=? AND company_id=? AND location_id=? AND active=1
      LIMIT 1
    ");
    $st->execute([$cf_wh_id,$companyId,$locationId]);
    $cfCode = (string)($st->fetchColumn() ?: '');
    if (!in_array($cfCode, ['CF1','CF2'], true)) {
      jexit(['ok'=>false,'error'=>'Destino CF debe ser CF1 o CF2'], 409);
    }

    $whCUB = wh_id_by_code($pdo,$companyId,$locationId,'CUB');
    if ($q_cub>0 && $whCUB<=0) jexit(['ok'=>false,'error'=>'Falta warehouse CUB'], 500);

    $st = $pdo->prepare("
      SELECT id
      FROM quality_concepts
      WHERE company_id=? AND concept_type='MERMA' AND active=1
      ORDER BY sort_order, id
      LIMIT 1
    ");
    $st->execute([$companyId]);
    $mermaConceptId = (int)($st->fetchColumn() ?: 1);

    $detail = get_capture_detail($pdo, $companyId, $locationId, $captureUid);
    if (!$detail) jexit(['ok'=>false,'error'=>'Captura no encontrada'], 404);

    $shift = (int)$detail['shift'];
    $captureGroupId = 0;
    if (strpos($captureUid, 'LEG-') !== 0) {
      $captureGroupId = get_capture_group_id($pdo, $companyId, $locationId, $captureUid);
      if ($captureGroupId <= 0) jexit(['ok'=>false,'error'=>'No se encontró el grupo de captura para actualizar inventario.'], 404);
    }

    $pdo->beginTransaction();
    try {
      if (strpos($captureUid, 'LEG-') !== 0) {
        $st = $pdo->prepare("
          UPDATE production_capture_groups
          SET product_id=?, entry_time=?, cf_wh_id=?, qty_total=?, qty_cf=?, qty_cub=?, qty_merma=?, updated_by=?, updated_at=NOW()
          WHERE capture_uid=? AND company_id=? AND location_id=?
        ");
        $st->execute([
          $productId, $entryTime, $cf_wh_id, $total, $q_cf, $q_cub, $q_mer, (int)$u['id'],
          $captureUid, $companyId, $locationId
        ]);

        $st = $pdo->prepare("DELETE FROM production_entries WHERE capture_uid=? AND company_id=? AND location_id=?");
        $st->execute([$captureUid,$companyId,$locationId]);

        $st = $pdo->prepare("DELETE FROM quality_entries WHERE capture_uid=? AND company_id=? AND location_id=? AND concept_type='MERMA'");
        $st->execute([$captureUid,$companyId,$locationId]);

        delete_production_inventory_moves_by_group($pdo, $companyId, $locationId, $captureGroupId);
      } else {
        $oldEntryTime = $detail['entry_time'];
        $oldProductId = (int)$detail['product_id'];

        $st = $pdo->prepare("
          DELETE FROM production_entries
          WHERE company_id=? AND location_id=? AND work_date=? AND shift=? AND product_id=? AND entry_time=?
        ");
        $st->execute([$companyId,$locationId,$date,$shift,$oldProductId,$oldEntryTime]);

        $st = $pdo->prepare("
          DELETE FROM quality_entries
          WHERE company_id=? AND location_id=? AND work_date=? AND shift=? AND product_id=? AND concept_type='MERMA'
            AND capture_uid IS NULL
            AND created_at BETWEEN DATE_SUB(?, INTERVAL 2 MINUTE) AND DATE_ADD(?, INTERVAL 2 MINUTE)
        ");
        $st->execute([$companyId,$locationId,$date,$shift,$oldProductId,$oldEntryTime,$oldEntryTime]);

        $captureUid = 'CAP-'.date('YmdHis').'-'.bin2hex(random_bytes(4));
        $stCG = $pdo->prepare("
          INSERT INTO production_capture_groups(
            capture_uid, company_id, location_id, work_date, shift, product_id, entry_time, cf_wh_id,
            qty_total, qty_cf, qty_cub, qty_merma, created_by, created_at, updated_by, updated_at
          )
          VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),?,NOW())
        ");
        $stCG->execute([
          $captureUid, $companyId, $locationId, $date, $shift, $productId, $entryTime, $cf_wh_id,
          $total, $q_cf, $q_cub, $q_mer, (int)$u['id'], (int)$u['id']
        ]);
        $captureGroupId = (int)$pdo->lastInsertId();
      }

      $ins = $pdo->prepare("
        INSERT INTO production_entries(
          company_id,location_id,work_date,shift,product_id,warehouse_id,qty,entry_time,capture_uid,created_by
        ) VALUES(?,?,?,?,?,?,?,?,?,?)
      ");
      if ($q_cf>0) $ins->execute([$companyId,$locationId,$date,$shift,$productId,$cf_wh_id,$q_cf,$entryTime,$captureUid,(int)$u['id']]);
      if ($q_cub>0) $ins->execute([$companyId,$locationId,$date,$shift,$productId,$whCUB,$q_cub,$entryTime,$captureUid,(int)$u['id']]);

      insert_production_inventory_moves_for_capture(
        $pdo, $companyId, $locationId, $date, $productId, $cf_wh_id, $whCUB,
        $q_cf, $q_cub, $q_mer, $captureUid, $captureGroupId, (int)$u['id']
      );

      if ($q_mer>0) {
        $stQ = $pdo->prepare("
          INSERT INTO quality_entries(
            company_id,location_id,work_date,shift,product_id,concept_type,concept_id,qty,notes,capture_uid,created_by,created_at
          ) VALUES(?,?,?,?,?,'MERMA',?,?,NULL,?,?,NOW())
        ");
        $stQ->execute([$companyId,$locationId,$date,$shift,$productId,$mermaConceptId,$q_mer,$captureUid,(int)$u['id']]);
      }

      $pdo->commit();
    } catch (Throwable $e) {
      $pdo->rollBack();
      jexit(['ok'=>false,'error'=>'Error actualizando captura: '.$e->getMessage()], 500);
    }

    recalc_day($pdo, $companyId, $locationId, $date);
    jexit(['ok'=>true]);
  }

  if ($action === 'capture_delete') {
    $d = read_json();
    $captureUid = (string)($d['capture_uid'] ?? '');
    if ($captureUid === '') jexit(['ok'=>false,'error'=>'capture_uid requerido'], 400);

    $detail = get_capture_detail($pdo, $companyId, $locationId, $captureUid);
    if (!$detail) jexit(['ok'=>false,'error'=>'Captura no encontrada'], 404);

    $workDate = (string)$detail['work_date'];
    $prodDayId = ensure_prod_day($pdo, $companyId, $locationId, $workDate, (int)$u['id']);
    $st = $pdo->prepare("SELECT locked FROM production_day WHERE id=?");
    $st->execute([$prodDayId]);
    if ((int)$st->fetchColumn() === 1) jexit(['ok'=>false,'error'=>'Día bloqueado.'], 409);

    $pdo->beginTransaction();
    try {
      if (strpos($captureUid, 'LEG-') !== 0) {
        $captureGroupId = get_capture_group_id($pdo, $companyId, $locationId, $captureUid);
        delete_production_inventory_moves_by_group($pdo, $companyId, $locationId, $captureGroupId);

        $st = $pdo->prepare("DELETE FROM production_entries WHERE capture_uid=? AND company_id=? AND location_id=?");
        $st->execute([$captureUid,$companyId,$locationId]);

        $st = $pdo->prepare("DELETE FROM quality_entries WHERE capture_uid=? AND company_id=? AND location_id=?");
        $st->execute([$captureUid,$companyId,$locationId]);

        $st = $pdo->prepare("DELETE FROM production_capture_groups WHERE capture_uid=? AND company_id=? AND location_id=?");
        $st->execute([$captureUid,$companyId,$locationId]);
      } else {
        $st = $pdo->prepare("
          DELETE FROM production_entries
          WHERE company_id=? AND location_id=? AND work_date=? AND shift=? AND product_id=? AND entry_time=?
        ");
        $st->execute([
          $companyId,$locationId,$detail['work_date'],$detail['shift'],$detail['product_id'],$detail['entry_time']
        ]);

        $st = $pdo->prepare("
          DELETE FROM quality_entries
          WHERE company_id=? AND location_id=? AND work_date=? AND shift=? AND product_id=? AND concept_type='MERMA'
            AND capture_uid IS NULL
            AND created_at BETWEEN DATE_SUB(?, INTERVAL 2 MINUTE) AND DATE_ADD(?, INTERVAL 2 MINUTE)
        ");
        $st->execute([
          $companyId,$locationId,$detail['work_date'],$detail['shift'],$detail['product_id'],
          $detail['entry_time'],$detail['entry_time']
        ]);
      }

      $pdo->commit();
    } catch (Throwable $e) {
      $pdo->rollBack();
      jexit(['ok'=>false,'error'=>'Error borrando captura: '.$e->getMessage()], 500);
    }

    recalc_day($pdo, $companyId, $locationId, $workDate);
    jexit(['ok'=>true]);
  }

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

/* ===================== UI ===================== */
$csrf = csrf_token();

$st = $pdo->prepare("SELECT name FROM companies WHERE id=?");
$st->execute([$companyId]);
$companyName = (string)($st->fetchColumn() ?: '');

$st = $pdo->prepare("SELECT name FROM locations WHERE id=?");
$st->execute([$locationId]);
$locationName = (string)($st->fetchColumn() ?: '');
?>
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>BReport · Producción</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.min.css" rel="stylesheet">

  <style>
    :root{
      --bg1:#f6f9fc;
      --bg2:#ffffff;
      --panel:#ffffff;
      --panel2:#fbfdff;
      --line:#d9e4f2;
      --txt:#16324f;
      --txt-soft:#35516f;
      --muted:#6f849a;
      --success:#10b981;
      --info:#2563eb;
      --warn:#f59e0b;
      --danger:#ef4444;
      --radius:24px;
      --shadow:0 18px 40px rgba(15,23,42,.10);
      --select-bg:#ffffff;
      --select-option-bg:#ffffff;
      --select-option-color:#16324f;
      --water:#38bdf8;
      --water2:#0ea5e9;
    }

    body{
      min-height:100vh;
      color:var(--txt);
      background:
        radial-gradient(1200px 650px at 0% 0%, rgba(56,189,248,.10), transparent 58%),
        radial-gradient(1000px 620px at 100% 0%, rgba(37,99,235,.08), transparent 55%),
        linear-gradient(180deg, var(--bg1), var(--bg2));
    }

    .mono{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;}
    .topbar{background:rgba(255,255,255,.92); border-bottom:1px solid var(--line); backdrop-filter: blur(12px);}
    .cardX{
      background:linear-gradient(180deg, rgba(255,255,255,.98), rgba(248,251,255,.96));
      border:1px solid var(--line);
      border-radius:var(--radius);
      box-shadow:var(--shadow);
      backdrop-filter: blur(10px);
    }
    .cardInner{
      background:rgba(247,250,252,.95);
      border:1px solid #e4edf7;
      border-radius:20px;
    }
    .muted{color:var(--muted);}
    .tiny{font-size:.78rem;}
    .smallx{font-size:.88rem;}
    .pill{
      border:1px solid var(--line);
      border-radius:999px;
      padding:.32rem .72rem;
      background:#f8fbff;
      color:var(--txt-soft);
    }
    .btn-accent{
      background:linear-gradient(135deg, rgba(45,212,191,.95), rgba(59,130,246,.92));
      border:0;
      color:#06111c;
      font-weight:700;
    }
    .btn-accent:hover{filter:brightness(1.06); color:#06111c;}
    .btn-ic{display:inline-flex; align-items:center; gap:.45rem;}

    .kpiTitle{font-size:.92rem; color:var(--txt-soft);}
    .kpiBig{font-size:2rem; font-weight:800; letter-spacing:.3px; line-height:1.1;}
    .kpiSub{font-size:.88rem; color:var(--muted);}

    .progress{height:11px; background:rgba(255,255,255,.08); border-radius:999px; overflow:hidden;}
    .progress-bar{background:linear-gradient(90deg, rgba(45,212,191,.95), rgba(59,130,246,.90));}
    .progress-bar.warn{background:linear-gradient(90deg, rgba(251,191,36,.95), rgba(245,158,11,.90));}
    .progress-bar.danger{background:linear-gradient(90deg, rgba(251,113,133,.95), rgba(239,68,68,.90));}

    .form-control, .form-select{
      background:#fff;
      border:1px solid #d7e3f1;
      color:var(--txt);
      border-radius:14px;
    }
    .form-control:focus, .form-select:focus{
      background:#fff;
      border-color:rgba(45,212,191,.55);
      box-shadow:0 0 0 .2rem rgba(45,212,191,.12);
      color:var(--txt);
    }

    .form-select{
      background-color:var(--select-bg) !important;
      color:var(--txt) !important;
    }
    .form-select option{
      background:var(--select-option-bg) !important;
      color:var(--select-option-color) !important;
    }
    .form-select optgroup{
      background:var(--select-option-bg) !important;
      color:var(--select-option-color) !important;
    }

    .tableWrap{
      background:#fbfdff;
      border:1px solid #e3ebf5;
      border-radius:18px;
      overflow:hidden;
    }

    .table{
      --bs-table-bg: transparent !important;
      --bs-table-color: var(--txt) !important;
      --bs-table-border-color: #e2e8f0 !important;
      --bs-table-accent-bg: transparent !important;
      --bs-table-striped-bg: #f8fbff !important;
      --bs-table-hover-bg: #f3f8ff !important;
      --bs-table-hover-color: var(--txt) !important;
      color:var(--txt) !important;
      margin-bottom:0;
      background:transparent !important;
    }
    .table thead, .table tbody, .table tr, .table th, .table td{
      background:transparent !important;
      color:var(--txt) !important;
      box-shadow:none !important;
    }
    .table thead th{
      color:#35516f!important;
      border-color:#e2e8f0!important;
      font-weight:700;
      white-space:nowrap;
    }
    .table td{
      border-color:#e2e8f0!important;
      vertical-align:middle;
    }
    .table tbody tr:hover{
      background:#f6fbff!important;
    }

    .stickyHead thead th{
      position:sticky;
      top:0;
      z-index:2;
      background:#f8fbff!important;
      backdrop-filter: blur(8px);
    }

    .divider{height:1px; background:rgba(255,255,255,.10); margin:14px 0;}

    .modal-content.darkModal{
      background:#ffffff;
      border:1px solid #dbe7f3;
      color:var(--txt);
      border-radius:26px;
      box-shadow:var(--shadow);
    }
    .modal-header, .modal-footer{border-color:#e2e8f0!important;}
    .shiftCard{
      cursor:pointer;
      transition:transform .16s ease, box-shadow .16s ease, border-color .16s ease;
      position:relative;
    }
    .shiftCard:hover{
      transform:translateY(-2px);
      border-color:#93c5fd;
      box-shadow:0 18px 46px rgba(37,99,235,.14);
    }
    .shiftCard:focus{
      outline:3px solid rgba(37,99,235,.18);
      outline-offset:2px;
    }
    .shiftCard .shiftHint{
      font-size:.72rem;
      color:#64748b;
      margin-top:8px;
      display:none;
    }
    .shiftCard:hover .shiftHint{display:block;}
    .summaryGrid{
      display:grid;
      grid-template-columns:repeat(auto-fit,minmax(190px,1fr));
      gap:12px;
    }
    .summaryTile{
      background:#f8fbff;
      border:1px solid #dbe7f3;
      border-radius:18px;
      padding:14px 16px;
    }
    .summaryTile .label{font-size:.82rem;color:#64748b;font-weight:700;}
    .summaryTile .value{font-size:1.45rem;font-weight:800;color:#16324f;}

    .captureSummary{
      display:grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap:12px;
    }
    .captureItem{
      padding:14px 16px;
      border-radius:18px;
      background:rgba(255,255,255,.035);
      border:1px solid #e3ebf5;
    }
    .captureLabel{color:var(--muted); font-size:.84rem;}
    .captureValue{font-size:1.15rem; font-weight:800;}

    
    .btn-outline-light{--bs-btn-color:#35516f; --bs-btn-border-color:#cdd8e5; --bs-btn-hover-bg:#eef5ff; --bs-btn-hover-color:#16324f; --bs-btn-hover-border-color:#b8c9db;}
    .btn-outline-info{--bs-btn-color:#2563eb; --bs-btn-border-color:#93c5fd; --bs-btn-hover-bg:#eff6ff; --bs-btn-hover-color:#1d4ed8;}
    .btn-outline-success{--bs-btn-color:#059669; --bs-btn-border-color:#86efac; --bs-btn-hover-bg:#ecfdf5; --bs-btn-hover-color:#047857;}
    .btn-outline-danger{--bs-btn-color:#dc2626; --bs-btn-border-color:#fca5a5; --bs-btn-hover-bg:#fef2f2; --bs-btn-hover-color:#b91c1c;}
    .text-bg-dark{background:#e8f0fb !important; color:#16324f !important;}
    .darkModal .btn-close{filter:none; opacity:.75;}
    .waterHero{display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:16px;}
    .waterTank{position:relative; min-height:260px; border-radius:26px; border:1px solid #cfe1f5; background:linear-gradient(180deg,#f9fcff,#eef6ff); overflow:hidden; box-shadow: inset 0 1px 0 rgba(255,255,255,.7);}
    .waterTankHead{padding:18px 18px 0; display:flex; justify-content:space-between; align-items:center; gap:10px; position:relative; z-index:3;}
    .waterTankBody{position:relative; height:160px; margin:10px 18px 0; border-radius:22px; border:2px solid #bfdbfe; background:linear-gradient(180deg,#f8fbff,#e8f3ff); overflow:hidden;}
    .waterFillProduced{position:absolute; left:0; right:0; bottom:0; height:0%; background:linear-gradient(180deg,rgba(56,189,248,.92),rgba(14,165,233,.96)); transition:height .45s ease;}
    .waterFillWasted{position:absolute; left:0; right:0; bottom:0; height:0%; background:linear-gradient(180deg,rgba(248,113,113,.94),rgba(220,38,38,.96)); transition:height .45s ease;}
    .waterFillProduced::before,.waterFillWasted::before{content:''; position:absolute; left:-10%; right:-10%; top:-12px; height:24px; background:radial-gradient(circle at 20% 50%, rgba(255,255,255,.55) 0 16%, transparent 17%), radial-gradient(circle at 55% 45%, rgba(255,255,255,.32) 0 12%, transparent 13%), radial-gradient(circle at 85% 55%, rgba(255,255,255,.42) 0 14%, transparent 15%); opacity:.75;}
    .waterLegend{display:flex; gap:10px; flex-wrap:wrap; margin-top:8px;}
    .waterLegendItem{display:inline-flex; align-items:center; gap:6px; font-size:.78rem; color:var(--muted); font-weight:700;}
    .waterDot{width:10px; height:10px; border-radius:999px; display:inline-block;}
    .waterDot.blue{background:#0ea5e9;}
    .waterDot.red{background:#dc2626;}
    .waterTankMetrics{padding:14px 18px 18px; position:relative; z-index:3;}
    .waterMetricRow{display:flex; justify-content:space-between; gap:12px; margin-top:8px;}
    .waterMini{font-size:.82rem; color:var(--muted);}
    .waterBig{font-size:1.35rem; font-weight:800;}
    .waterSummaryGrid{display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:12px;}
    .waterSummaryCard{padding:14px 16px; border-radius:18px; border:1px solid #dbe7f3; background:#f8fbff;}

    .signalFab{
      position:fixed;
      right:18px;
      bottom:18px;
      z-index:1050;
      border:0;
      border-radius:999px;
      padding:12px 18px;
      display:inline-flex;
      align-items:center;
      gap:10px;
      font-weight:800;
      box-shadow:0 18px 45px rgba(15,23,42,.24);
      background:linear-gradient(135deg,#0f172a,#2563eb);
      color:#fff;
    }
    .signalFab .dot{
      width:10px;
      height:10px;
      border-radius:999px;
      display:inline-block;
      background:#94a3b8;
      box-shadow:0 0 0 4px rgba(255,255,255,.18);
    }
    .signalFab.is-auto{
      background:linear-gradient(135deg,#065f46,#16a34a);
    }
    .signalFab.is-auto .dot{
      background:#22c55e;
    }
    .signalFab.is-manual{
      background:linear-gradient(135deg,#7c2d12,#f97316);
    }
    .signalFab.is-manual .dot{
      background:#facc15;
    }
    .signalFab:disabled{
      opacity:.7;
      cursor:not-allowed;
    }
    @media (max-width:575.98px){
      .signalFab{
        left:12px;
        right:12px;
        bottom:12px;
        justify-content:center;
      }
    }


    @media (max-width: 991.98px){
      .captureSummary{grid-template-columns: repeat(2, minmax(0, 1fr));}
      .kpiBig{font-size:1.7rem;}
    }
    @media (max-width: 575.98px){
      .captureSummary{grid-template-columns: 1fr;}
      .kpiBig{font-size:1.45rem;}
    }
  </style>
</head>
<body>
<nav class="navbar topbar">
  <div class="container-fluid px-3">
    <div class="d-flex align-items-center gap-2 flex-wrap">
      <span class="navbar-brand fw-bold mb-0" style="color:var(--txt);">BReport · Producción</span>
      <span class="pill small"><?=h($companyName)?> · <?=h($locationName)?></span>
      <span class="pill small" id="tzBadge"><i class="bi bi-clock-history"></i> <?=h($APP_TZ)?></span>
    </div>
    <div class="ms-auto d-flex gap-2 align-items-center flex-wrap">
      <a class="btn btn-sm btn-outline-light btn-ic" href="produccion_cubicado.php"><i class="bi bi-arrow-left"></i>Producción cubicado</a>
      <a class="btn btn-sm btn-outline-light btn-ic" href="dashboard.php"><i class="bi bi-speedometer2"></i>Dashboard</a>
      <button class="btn btn-sm btn-outline-info btn-ic" id="btnSettings"><i class="bi bi-gear"></i>Ajustes</button>
      <a class="btn btn-sm btn-outline-danger btn-ic" href="logout.php"><i class="bi bi-box-arrow-right"></i>Salir</a>
    </div>
  </div>
</nav>

<button type="button" class="signalFab is-manual" id="btnSignalModeFloat" title="Cambiar modo Manual / Automático PLC">
  <span class="dot"></span>
  <span id="signalModeFloatText">Modo Manual</span>
</button>

<div class="container-fluid px-3 py-3">
  <div class="row g-3">
    <div class="col-12 col-xl-4">
      <div class="cardX p-3 h-100">
        <div class="d-flex align-items-start justify-content-between gap-2 flex-wrap">
          <div>
            <div class="fw-bold fs-4">Control del día</div>
            <div class="muted small">Para reportar producción debes iniciar turno.</div>
          </div>
          <span class="badge text-bg-secondary" id="lockBadge">Editable</span>
        </div>

        <div class="row g-2 mt-2">
          <div class="col-12 col-md-6">
            <label class="form-label small muted mb-1">Fecha</label>
            <input type="date" class="form-control" id="datePick" value="<?=h(tz_today($APP_TZ))?>">
          </div>

          <div class="col-12 col-md-6">
            <label class="form-label small muted mb-1">Turno</label>
            <div class="d-flex gap-2">
              <button class="btn btn-outline-success w-100 btn-ic" id="btnStartShift">
                <i class="bi bi-play-fill"></i>Iniciar
              </button>
              <button class="btn btn-outline-danger w-100 btn-ic" id="btnEndShift" disabled>
                <i class="bi bi-stop-fill"></i>Finalizar
              </button>
            </div>
            <div class="d-flex justify-content-between align-items-center mt-2 gap-2 flex-wrap">
              <span class="badge text-bg-secondary" id="shiftBadge">Sin turno</span>
              <span class="muted small mono" id="shiftMeta"></span>
            </div>
          </div>

          <div class="col-12">
            <div class="d-grid gap-2">
              <button class="btn btn-accent w-100 py-2 fw-semibold btn-ic" id="btnAdd">
                <i class="bi bi-plus-circle"></i>Registrar producción
              </button>
              <button class="btn btn-outline-info w-100 py-2 fw-semibold btn-ic" id="btnWater">
                <i class="bi bi-droplet-half"></i>Consumo de agua
              </button>
            </div>
            <div class="muted small mt-2" id="mustShiftMsg" style="display:none;">
              <i class="bi bi-exclamation-triangle"></i> Debes iniciar turno para habilitar el registro.
            </div>
          </div>

          <div class="col-12" id="plcSignalPanel" style="display:none;">
            <div class="alert alert-warning mb-0">
              <div class="d-flex justify-content-between align-items-start gap-2 flex-wrap">
                <div>
                  <b><i class="bi bi-broadcast-pin"></i> Señal PLC detectada</b>
                  <div class="small" id="plcSignalText">Esperando decisión del operador.</div>
                </div>
                <span class="badge text-bg-dark mono" id="plcSignalBadge">1</span>
              </div>
            </div>
          </div>
        </div>

        <div class="divider"></div>

        <div class="row g-3">
          <div class="col-12">
            <div class="cardInner p-3">
              <div class="d-flex justify-content-between align-items-start gap-2">
                <div>
                  <div class="kpiTitle">Total Producción</div>
                  <div class="kpiBig mono" id="totalProd">0.000</div>
                </div>
                <div class="text-end">
                  <div class="kpiTitle">Diferencia</div>
                  <div class="kpiBig mono" id="diffVal">0.000</div>
                </div>
              </div>
              <div class="mt-3" id="warnBox"></div>
            </div>
          </div>

          <div class="col-12">
            <div class="cardInner p-3">
              <div class="d-flex justify-content-between align-items-center gap-2">
                <div>
                  <div class="kpiTitle">Merma del turno</div>
                  <div class="muted tiny">Turno <span class="mono" id="mermaTurnLabel">1</span></div>
                </div>
                <div class="kpiBig mono" id="mermaShift">0.000</div>
              </div>
            </div>
          </div>
        </div>

        <div class="divider"></div>

        <div class="muted small">
          Nota: objetivos, antenas, bloqueo y zona horaria se configuran en <b>Ajustes</b>.
        </div>
      </div>
    </div>

    <div class="col-12 col-xl-8">
      <div class="row g-3">
        <div class="col-12 col-md-6 col-lg-3">
          <div class="cardX p-3 h-100 shiftCard" data-shift-card="1" tabindex="0" role="button" aria-label="Ver resumen del turno 1">
            <div class="d-flex justify-content-between align-items-center">
              <div class="kpiTitle">Turno 1</div>
              <span class="badge text-bg-dark mono" id="p1">0%</span>
            </div>
            <div class="kpiBig mono mt-2" id="s1v">0.000</div>
            <div class="kpiSub mono" id="s1t">/ 400</div>
            <div class="progress mt-3"><div class="progress-bar" id="bar1" style="width:0%"></div></div>
            <div class="shiftHint"><i class="bi bi-eye"></i> Clic para ver resumen y descargar reporte</div>
          </div>
        </div>

        <div class="col-12 col-md-6 col-lg-3">
          <div class="cardX p-3 h-100 shiftCard" data-shift-card="2" tabindex="0" role="button" aria-label="Ver resumen del turno 2">
            <div class="d-flex justify-content-between align-items-center">
              <div class="kpiTitle">Turno 2</div>
              <span class="badge text-bg-dark mono" id="p2">0%</span>
            </div>
            <div class="kpiBig mono mt-2" id="s2v">0.000</div>
            <div class="kpiSub mono" id="s2t">/ 400</div>
            <div class="progress mt-3"><div class="progress-bar" id="bar2" style="width:0%"></div></div>
            <div class="shiftHint"><i class="bi bi-eye"></i> Clic para ver resumen y descargar reporte</div>
          </div>
        </div>

        <div class="col-12 col-md-6 col-lg-3">
          <div class="cardX p-3 h-100 shiftCard" data-shift-card="3" tabindex="0" role="button" aria-label="Ver resumen del turno 3">
            <div class="d-flex justify-content-between align-items-center">
              <div class="kpiTitle">Turno 3</div>
              <span class="badge text-bg-dark mono" id="p3">0%</span>
            </div>
            <div class="kpiBig mono mt-2" id="s3v">0.000</div>
            <div class="kpiSub mono" id="s3t">/ 400</div>
            <div class="progress mt-3"><div class="progress-bar" id="bar3" style="width:0%"></div></div>
            <div class="shiftHint"><i class="bi bi-eye"></i> Clic para ver resumen y descargar reporte</div>
          </div>
        </div>

        <div class="col-12 col-md-6 col-lg-3">
          <div class="cardX p-3 h-100 dayTotalCard" data-day-total-card="1" tabindex="0" role="button" aria-label="Ver concentrado del día">
            <div class="d-flex justify-content-between align-items-center">
              <div class="kpiTitle">Total del día</div>
              <span class="badge text-bg-dark mono" id="pt">0%</span>
            </div>
            <div class="kpiBig mono mt-2" id="stv">0.000</div>
            <div class="kpiSub mono" id="stt">/ 1200</div>
            <div class="progress mt-3"><div class="progress-bar" id="bart" style="width:0%"></div></div>
            <div class="shiftHint"><i class="bi bi-file-earmark-pdf"></i> Clic para descargar concentrado diario</div>
          </div>
        </div>
      </div>

      <div class="cardX p-3 mt-3">
        <div class="d-flex align-items-center justify-content-between gap-2 flex-wrap">
          <div>
            <div class="fw-semibold fs-5">Totales por almacén destino</div>
            <div class="muted small" id="whSubtitle">Acumulado de capturas OK por cuarto / proceso del turno</div>
          </div>
          <span class="pill mono" id="whCount">0 almacenes</span>
        </div>

        <div class="row g-3 mt-1" id="whCards"></div>

        <div class="tableWrap mt-3">
          <div class="table-responsive stickyHead">
            <table class="table table-sm align-middle mb-0">
              <thead>
                <tr>
                  <th>Almacén</th>
                  <th class="text-end mono">Qty OK</th>
                </tr>
              </thead>
              <tbody id="whBody"></tbody>
            </table>
          </div>
        </div>
      </div>

      <div class="cardX p-3 mt-3">
        <div class="d-flex align-items-center justify-content-between gap-2 flex-wrap">
          <div>
            <div class="fw-semibold fs-5">Bitácora de capturas</div>
            <div class="muted small" id="logSubtitle">Se muestra un solo registro por captura del turno seleccionado. Puedes ver detalle, editar o borrar si el día no está bloqueado.</div>
          </div>
          <span class="pill mono" id="logCount">0 capturas</span>
        </div>

        <div class="tableWrap mt-3">
          <div class="table-responsive stickyHead">
            <table class="table table-sm align-middle mb-0">
              <thead>
                <tr>
                  <th>Hora</th>
                  <th>Turno</th>
                  <th>Producto</th>
                  <th>Destino CF</th>
                  <th class="text-end mono">CF</th>
                  <th class="text-end mono">CUB</th>
                  <th class="text-end mono">Merma</th>
                  <th class="text-end">Acciones</th>
                </tr>
              </thead>
              <tbody id="logBody"></tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Modales -->

<div class="modal fade" id="mdlShiftSummary" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-clipboard-data"></i> Resumen de producción por turno</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="shiftSummaryBody"></div>
      <div class="modal-footer" id="shiftSummaryFooter"></div>
    </div>
  </div>
</div>

<div class="modal fade" id="mdlShift" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-play-fill"></i> Iniciar turno</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="muted small">Selecciona el turno a trabajar para la fecha elegida.</div>
        <label class="form-label small muted mt-3">Turno</label>
        <select class="form-select" id="shiftSel">
          <option value="1">Turno 1 (07:00 - 15:00)</option>
          <option value="2">Turno 2 (15:00 - 22:30)</option>
          <option value="3">Turno 3 (22:30 - 07:00)</option>
        </select>
        <div class="alert alert-info mt-3 mb-0">Al iniciar turno se habilita <b>Registrar producción</b>.</div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-accent" id="doShiftStart">Iniciar</button>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="mdlSettings" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-lg modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-gear"></i> Ajustes del día</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="row g-3">
          <div class="col-12">
            <div class="fw-semibold">Zona horaria</div>
            <div class="muted small">Recomendado para Reynosa: <b>America/Matamoros</b>.</div>
          </div>
          <div class="col-12 col-md-8">
            <select class="form-select" id="tzSel"></select>
          </div>
          <div class="col-12 col-md-4">
            <button class="btn btn-outline-info w-100 btn-ic" id="btnSaveTz"><i class="bi bi-clock"></i>Guardar TZ</button>
          </div>

          <hr class="border-light opacity-25 my-2">

          <div class="col-12">
            <div class="fw-semibold">Objetivos</div>
            <div class="muted small">Edita meta por turno y total del día.</div>
          </div>

          <div class="col-12 col-md-3"><div class="kpiTitle">Target T1</div><input type="number" step="1" class="form-control mono" id="t1"></div>
          <div class="col-12 col-md-3"><div class="kpiTitle">Target T2</div><input type="number" step="1" class="form-control mono" id="t2"></div>
          <div class="col-12 col-md-3"><div class="kpiTitle">Target T3</div><input type="number" step="1" class="form-control mono" id="t3"></div>
          <div class="col-12 col-md-3"><div class="kpiTitle">Target Total</div><input type="number" step="1" class="form-control mono" id="tt"></div>

          <div class="col-12">
            <button class="btn btn-outline-light w-100 btn-ic" id="btnSaveTargets"><i class="bi bi-save"></i>Guardar objetivos</button>
          </div>

          <hr class="border-light opacity-25 my-2">

          <div class="col-12">
            <div class="fw-semibold">Antenas (manual)</div>
            <div class="muted small">Se usa para comparación contra producción total.</div>
          </div>
          <div class="col-12">
            <div class="input-group">
              <input type="number" step="0.001" class="form-control mono" id="antennaVal" value="0">
              <button class="btn btn-outline-info btn-ic" id="saveAntenna"><i class="bi bi-save2"></i>Guardar</button>
            </div>
          </div>

          <hr class="border-light opacity-25 my-2">

          <div class="col-12">
            <div class="fw-semibold"><i class="bi bi-broadcast-pin"></i> Reportaje manual / automático por señal</div>
            <div class="muted small">El PLC solo actualiza el estado del tag a 1. La aplicación detecta la señal y abre el modal de reportaje para registrar la cosecha.</div>
          </div>
          <div class="col-12 col-md-4">
            <label class="form-label small muted">Modo</label>
            <select class="form-select" id="sigMode">
              <option value="MANUAL">Manual</option>
              <option value="AUTOMATIC">Automático por señal</option>
            </select>
          </div>
          <div class="col-12 col-md-4">
            <label class="form-label small muted">Lectura PLC</label>
            <select class="form-select" id="sigEnabled">
              <option value="0">Desactivada</option>
              <option value="1">Activada</option>
            </select>
          </div>
          <div class="col-12 col-md-4">
            <label class="form-label small muted">Revisión cada segundos</label>
            <input type="number" min="3" max="60" step="1" class="form-control mono" id="sigPoll" value="5">
          </div>
          <div class="col-12 col-md-4">
            <label class="form-label small muted">Producto default</label>
            <select class="form-select" id="sigProduct"></select>
          </div>
          <div class="col-12 col-md-4">
            <label class="form-label small muted">CF default</label>
            <select class="form-select" id="sigCf"></select>
          </div>
          <div class="col-12 col-md-4">
            <label class="form-label small muted">Cantidad default</label>
            <input type="number" min="1" step="1" class="form-control mono" id="sigQty" value="10">
          </div>
          <div class="col-12">
            <button class="btn btn-outline-info w-100 btn-ic" id="btnSaveSignalSettings"><i class="bi bi-save"></i>Guardar configuración de señal</button>
          </div>

          <div class="col-12">
            <div class="cardInner p-3">
              <div class="d-flex justify-content-between align-items-center gap-2 flex-wrap mb-2">
                <div>
                  <div class="fw-semibold">Tags PLC configurados</div>
                  <div class="muted small">Ejemplo: Taga_linea1, Taga_linea2, Taga_linea3. El PLC debe actualizar signal_state de 0 a 1.</div>
                </div>
              </div>
              <div class="row g-2 mb-2">
                <input type="hidden" id="sigTagId" value="0">
                <div class="col-12 col-md-3"><input class="form-control" id="sigTagCode" placeholder="Tag PLC"></div>
                <div class="col-12 col-md-3"><input class="form-control" id="sigTagName" placeholder="Nombre visible"></div>
                <div class="col-12 col-md-3"><input class="form-control" id="sigLineName" placeholder="Línea"></div>
                <div class="col-12 col-md-2">
                  <select class="form-select" id="sigTagActive"><option value="1">Activo</option><option value="0">Inactivo</option></select>
                </div>
                <div class="col-12 col-md-1"><button class="btn btn-outline-light w-100" id="btnSaveSignalTag"><i class="bi bi-plus-lg"></i></button></div>
              </div>
              <div class="table-responsive">
                <table class="table table-sm align-middle mb-0">
                  <thead><tr><th>Tag</th><th>Nombre</th><th>Línea</th><th>Estado</th><th>Señal</th><th></th></tr></thead>
                  <tbody id="sigTagsBody"><tr><td colspan="6" class="muted">Sin tags.</td></tr></tbody>
                </table>
              </div>
            </div>
          </div>

          <hr class="border-light opacity-25 my-2">

          <div class="col-12">
            <div class="fw-semibold">Bloqueo del día</div>
            <div class="muted small">Bloquear impide capturar o modificar registros.</div>
            <?php if (has_role('admin') || has_role('plant_admin')): ?>
              <button class="btn btn-outline-warning w-100 btn-ic mt-2" id="btnLock"><i class="bi bi-lock-fill"></i>Bloquear / Desbloquear</button>
            <?php else: ?>
              <div class="alert alert-secondary mt-2 mb-0">Solo admin / plant_admin puede bloquear.</div>
            <?php endif; ?>
          </div>
        </div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" data-bs-dismiss="modal">Cerrar</button>
      </div>
    </div>
  </div>
</div>


<div class="modal fade" id="mdlWater" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-droplet-half"></i> Consumo de agua por turno</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="alert alert-info mb-3" id="waterNote">Simulación de consumo de agua.</div>
        <div class="waterSummaryGrid mb-3" id="waterTopCards"></div>
        <div class="waterHero" id="waterTanks"></div>
        <div class="tableWrap mt-3">
          <div class="table-responsive">
            <table class="table table-sm align-middle mb-0">
              <thead>
                <tr>
                  <th>Turno</th>
                  <th class="text-end mono">Reportado</th>
                  <th class="text-end mono">Barras equivalentes</th>
                  <th class="text-end mono">Objetivo barras</th>
                  <th class="text-end mono">Agua producida</th>
                  <th class="text-end mono">Agua mermada</th>
                  <th class="text-end mono">Litros utilizados</th>
                  <th class="text-end mono">Objetivo litros</th>
                  <th class="text-end mono">Almacén</th>
                  <th class="text-end mono">CUB</th>
                  <th class="text-end mono">Merma</th>
                </tr>
              </thead>
              <tbody id="waterBody"></tbody>
            </table>
          </div>
        </div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" data-bs-dismiss="modal">Cerrar</button>
      </div>
    </div>
  </div>
</div>


<div class="modal fade" id="mdlSignal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-broadcast-pin"></i> Señal detectada por sensor</h5>
      </div>
      <div class="modal-body">
        <div class="alert alert-warning mb-3">
          El sensor cambió de <b>0 a 1</b>. Revisa y confirma el reportaje de producción.
        </div>
        <div class="cardInner p-3 mb-3">
          <div class="d-flex justify-content-between"><span>Tag</span><b class="mono" id="sigModalTag">—</b></div>
          <div class="d-flex justify-content-between"><span>Línea</span><b id="sigModalLine">—</b></div>
          <div class="d-flex justify-content-between"><span>Hora detectada</span><b class="mono" id="sigModalTime">—</b></div>
        </div>
        <div style="display:none;">
          <label class="form-label small muted">Comentario para mantenimiento</label>
          <textarea class="form-control" id="sigMaintNotes" rows="2" placeholder="Mantenimiento oculto temporalmente"></textarea>
        </div>
      </div>
      <div class="modal-footer d-flex justify-content-end flex-wrap gap-2">
        <button class="btn btn-outline-warning d-none" id="btnSignalMaintenance" type="button"><i class="bi bi-tools"></i> Caída por mantenimiento</button>
        <button class="btn btn-accent" id="btnSignalProduction"><i class="bi bi-plus-circle"></i> Reportar producción</button>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="mdl" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-lg modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-plus-circle"></i> Reportar producción</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="alert alert-info mb-3">Se registrará en el <b>turno activo</b>.</div>

        <div class="row g-3">
          <div class="col-12 col-md-4">
            <label class="form-label small muted">Hora</label>
            <input type="datetime-local" class="form-control mono" id="mTime">
          </div>

          <div class="col-12 col-md-8">
            <label class="form-label small muted">Producto</label>
            <select class="form-select" id="mProd"></select>
          </div>

          <div class="col-12">
            <div class="cardInner p-3">
              <div class="d-flex justify-content-between align-items-center gap-2 flex-wrap">
                <div class="fw-semibold">Distribución</div>
                <div class="pill mono">Total: <span id="mTotalLbl">10</span></div>
              </div>

              <div class="row g-3 mt-1">
                <div class="col-12 col-md-3">
                  <label class="form-label small muted">Total cosecha</label>
                  <input type="number" step="1" class="form-control mono" id="mTotal" value="10" min="10" max="10" readonly aria-readonly="true">
                  <div class="form-text muted">Fijo: 10 por cosecha.</div>
                </div>

                <div class="col-12 col-md-3">
                  <label class="form-label small muted">Destino CF</label>
                  <select class="form-select" id="mCF"></select>
                </div>

                <div class="col-12 col-md-2">
                  <label class="form-label small muted">Qty CF</label>
                  <input type="number" step="1" class="form-control mono" id="qCF" value="10" min="0" readonly>
                </div>

                <div class="col-12 col-md-2">
                  <label class="form-label small muted">CUB</label>
                  <input type="number" step="1" class="form-control mono" id="qCUB" value="0" min="0">
                </div>

                <div class="col-12 col-md-2">
                  <label class="form-label small muted">Merma</label>
                  <input type="number" step="1" class="form-control mono" id="qMER" value="0" min="0">
                </div>

                <div class="col-12">
                  <div class="alert alert-secondary mb-0">
                    <b>Regla:</b> CF + CUB + Merma = Total.<br>
                    <span class="mono">Suma:</span> <b class="mono" id="sumLbl">10</b>
                    &nbsp;|&nbsp;
                    <span class="mono">Diferencia:</span> <b class="mono" id="diffLbl">0</b>
                  </div>
                </div>
              </div>
            </div>
          </div>

        </div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-accent" id="mSave"><i class="bi bi-check2"></i> Guardar</button>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="mdlConfirm" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-shield-check"></i> Confirmar registro</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="confirmBody"></div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-accent" id="confirmYes">Confirmar</button>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="mdlEnd" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-xl modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-stopwatch"></i> Finalizar turno</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="endBody"></div>
      <div class="modal-footer" id="endFooter"></div>
    </div>
  </div>
</div>

<div class="modal fade" id="mdlViewCapture" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-xl modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-eye"></i> Detalle de captura</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div id="viewCaptureBody"></div>

        <div class="divider"></div>

        <div class="fw-semibold mb-2">Editar movimiento</div>
        <div class="row g-3">
          <input type="hidden" id="eCaptureUid">

          <div class="col-12 col-md-3">
            <label class="form-label small muted">Fecha</label>
            <input type="date" class="form-control" id="eDate" readonly>
          </div>

          <div class="col-12 col-md-3">
            <label class="form-label small muted">Hora</label>
            <input type="datetime-local" class="form-control mono" id="eTime">
          </div>

          <div class="col-12 col-md-6">
            <label class="form-label small muted">Producto</label>
            <select class="form-select" id="eProd"></select>
          </div>

          <div class="col-12 col-md-3">
            <label class="form-label small muted">Total</label>
            <input type="number" step="1" class="form-control mono" id="eTotal" min="1">
          </div>

          <div class="col-12 col-md-3">
            <label class="form-label small muted">Destino CF</label>
            <select class="form-select" id="eCF"></select>
          </div>

          <div class="col-12 col-md-2">
            <label class="form-label small muted">Qty CF</label>
            <input type="number" step="1" class="form-control mono" id="eQCF" readonly>
          </div>

          <div class="col-12 col-md-2">
            <label class="form-label small muted">CUB</label>
            <input type="number" step="1" class="form-control mono" id="eQCUB" min="0">
          </div>

          <div class="col-12 col-md-2">
            <label class="form-label small muted">Merma</label>
            <input type="number" step="1" class="form-control mono" id="eQMER" min="0">
          </div>

          <div class="col-12">
            <div class="alert alert-secondary mb-0">
              <b>Regla:</b> CF + CUB + Merma = Total.
              <br><span class="mono">Suma:</span> <b class="mono" id="eSumLbl">0</b>
              &nbsp;|&nbsp;
              <span class="mono">Diferencia:</span> <b class="mono" id="eDiffLbl">0</b>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-footer justify-content-between">
        <button class="btn btn-outline-danger" id="btnDeleteCapture"><i class="bi bi-trash"></i> Borrar captura</button>
        <div class="d-flex gap-2">
          <button class="btn btn-outline-light" data-bs-dismiss="modal">Cerrar</button>
          <button class="btn btn-accent" id="btnUpdateCapture"><i class="bi bi-save2"></i> Guardar cambios</button>
        </div>
      </div>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const CSRF = <?=json_encode($csrf)?>;
const APP_TZ = <?=json_encode($APP_TZ)?>;

const $ = (id)=>document.getElementById(id);
function h(s){return String(s??'').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}
function num(v){ const n=parseFloat(v); return isNaN(n)?0:n; }
function clamp(n,min,max){ return Math.max(min, Math.min(max,n)); }

async function apiGet(action, params={}){
  const u = new URL(location.href);
  u.search = '';
  u.searchParams.set('action', action);
  for(const [k,v] of Object.entries(params)){ u.searchParams.set(k, v); }
  const r = await fetch(u.toString(), {credentials:'same-origin'});
  const txt = await r.text();
  try { return JSON.parse(txt); } catch(e){ console.error(txt); return {ok:false,error:'NO_JSON'}; }
}
async function apiPost(action, payload){
  const u = new URL(location.href);
  u.search = '';
  u.searchParams.set('action', action);
  const r = await fetch(u.toString(), {
    method:'POST',
    credentials:'same-origin',
    headers:{'Content-Type':'application/json','X-CSRF':CSRF},
    body: JSON.stringify(payload || {})
  });
  const txt = await r.text();
  try { return JSON.parse(txt); } catch(e){ console.error(txt); return {ok:false,error:'NO_JSON'}; }
}

let products = [];
let warehouses = [];
let day = null;
let byShift = {1:0,2:0,3:0};
let byWh = [];
let captures = [];
let running = null;
let hbTimer = null;
let mermaByShift = {1:0,2:0,3:0};
let mermaCurrent = 0;
let displayShift = 1;
let selectedShift = 1;
let tzOptions = {};
let currentTz = APP_TZ;
let pendingBundle = null;
let waterSummary = null;
let signalSettings = null;
let signalTags = [];
let signalPollTimer = null;
let activeSignalTag = null;
const HARVEST_QTY = 10;

function setNowLocal(id){
  const d = new Date();
  const pad = (n)=>String(n).padStart(2,'0');
  const s = d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+'T'+pad(d.getHours())+':'+pad(d.getMinutes());
  $(id).value = s;
}
function sqlDateTimeToLocalInput(v){
  const s = String(v || '').replace(' ', 'T');
  return s.length >= 16 ? s.substring(0,16) : s;
}
function pct(val, target){
  target = num(target);
  if (target <= 0) return 0;
  return (num(val) / target) * 100;
}
function barClass(p){
  if (p < 70) return 'danger';
  if (p < 90) return 'warn';
  return '';
}
function setBar(el, p){
  const bar = $(el);
  const pc = clamp(p, 0, 200);
  bar.style.width = pc.toFixed(1) + '%';
  bar.classList.remove('warn','danger');
  const cls = barClass(p);
  if (cls) bar.classList.add(cls);
}
function setPctBadge(el, p){
  $(el).textContent = `${Math.round(p)}%`;
}

function applyShiftUI(){
  const has = !!running;
  const badge = $('shiftBadge');
  const meta  = $('shiftMeta');

  if (!has){
    badge.className = 'badge text-bg-secondary';
    badge.textContent = 'Sin turno';
    meta.textContent = '';
  } else {
    badge.className = 'badge text-bg-success';
    badge.textContent = `T${running.shift} activo`;
    meta.textContent = `Inicio: ${String(running.start_at||'').replace('T',' ')}`;
  }

  $('btnEndShift').disabled = !has;

  const locked = !!num(day?.locked || 0);
  $('btnAdd').disabled = !(has && !locked);
  $('mustShiftMsg').style.display = has ? 'none' : 'block';

  $('lockBadge').textContent = locked ? 'Bloqueado' : 'Editable';
  $('lockBadge').className = locked ? 'badge text-bg-warning' : 'badge text-bg-secondary';

  $('btnSaveTargets').disabled = locked;
  $('saveAntenna').disabled = locked;
  $('t1').disabled = locked;
  $('t2').disabled = locked;
  $('t3').disabled = locked;
  $('tt').disabled = locked;
  $('antennaVal').disabled = locked;

  if (hbTimer){ clearInterval(hbTimer); hbTimer = null; }
  if (has){
    hbTimer = setInterval(()=> apiPost('shift_heartbeat', {date: $('datePick').value}), 25000);
  }
}

function renderHeaderTotals(){
  const antenna = num($('antennaVal').value);
  const total = num(day?.total_prod_qty || 0);
  const diff  = num(day?.diff_qty || 0);

  $('totalProd').textContent = total.toFixed(3);
  $('diffVal').textContent = diff.toFixed(3);

  const warn = !!num(day?.warn_flag || 0);
  $('warnBox').innerHTML = warn
    ? `<div class="alert alert-warning py-2 mb-0"><b>Warning:</b> Producción (${total.toFixed(3)}) ${diff > 0 ? '+' : ''}${diff.toFixed(3)} vs Antenas (${antenna.toFixed(3)}).</div>`
    : `<div class="alert alert-success py-2 mb-0">OK: Producción coincide con Antenas.</div>`;

  $('mermaShift').textContent = num(mermaCurrent).toFixed(3);
  $('mermaTurnLabel').textContent = String(selectedShift || 1);
}

function renderTargets(){
  $('t1').value = num(day?.target_t1 || 400).toFixed(0);
  $('t2').value = num(day?.target_t2 || 400).toFixed(0);
  $('t3').value = num(day?.target_t3 || 400).toFixed(0);
  $('tt').value = num(day?.target_total || 1200).toFixed(0);
}

function renderKpis(){
  const t1 = num(day?.target_t1 ?? 400);
  const t2 = num(day?.target_t2 ?? 400);
  const t3 = num(day?.target_t3 ?? 400);
  const tt = num(day?.target_total ?? (t1+t2+t3));

  const v1 = num(byShift[1] ?? 0);
  const v2 = num(byShift[2] ?? 0);
  const v3 = num(byShift[3] ?? 0);
  const vT = v1+v2+v3;

  $('s1v').textContent = v1.toFixed(3); $('s1t').textContent = `/ ${t1.toFixed(0)}`;
  $('s2v').textContent = v2.toFixed(3); $('s2t').textContent = `/ ${t2.toFixed(0)}`;
  $('s3v').textContent = v3.toFixed(3); $('s3t').textContent = `/ ${t3.toFixed(0)}`;
  $('stv').textContent = vT.toFixed(3); $('stt').textContent = `/ ${tt.toFixed(0)}`;

  const p1 = pct(v1,t1), p2 = pct(v2,t2), p3 = pct(v3,t3), pT = pct(vT,tt);
  setPctBadge('p1', p1); setPctBadge('p2', p2); setPctBadge('p3', p3); setPctBadge('pt', pT);
  setBar('bar1', p1); setBar('bar2', p2); setBar('bar3', p3); setBar('bart', pT);
}

function renderWh(){
  const tb = $('whBody');
  tb.innerHTML = '';
  $('whCount').textContent = `${byWh.length} almacenes`;
  $('whSubtitle').textContent = `Acumulado de capturas OK por cuarto / proceso · Turno ${selectedShift}`;

  if (!byWh.length){
    tb.innerHTML = `<tr><td colspan="2" class="muted">Sin capturas en este turno.</td></tr>`;
    $('whCards').innerHTML = `<div class="col-12 muted small">Sin capturas por almacén en el turno ${selectedShift}.</div>`;
    return;
  }

  const cards = [];
  for(const r of byWh){
    const qty = num(r.qty_ok).toFixed(3);

    cards.push(`
      <div class="col-12 col-md-6 col-lg-4">
        <div class="cardInner p-3 h-100">
          <div class="kpiTitle">${h(r.code)} · ${h(r.name)}</div>
          <div class="kpiBig mono mt-1">${qty}</div>
          <div class="kpiSub">OK turno ${selectedShift}</div>
        </div>
      </div>
    `);

    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td>${h(r.code)} · ${h(r.name)}</td>
        <td class="mono text-end">${qty}</td>
      </tr>
    `);
  }

  $('whCards').innerHTML = cards.join('');
}

function renderLog(){
  const tb = $('logBody');
  tb.innerHTML = '';
  $('logCount').textContent = `${captures.length} capturas`;
  $('logSubtitle').textContent = `Se muestra un solo registro por captura del turno ${selectedShift}. Puedes ver detalle, editar o borrar si el día no está bloqueado.`;

  const locked = !!num(day?.locked || 0);

  if (!captures.length){
    tb.innerHTML = `<tr><td colspan="8" class="muted">Aún no hay capturas en el turno ${selectedShift}.</td></tr>`;
    return;
  }

  for(const c of captures){
    const legacyBadge = c.is_legacy ? `<div class="muted tiny">Histórico</div>` : '';
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="mono">${h(String(c.entry_time || '').replace('T',' '))}</td>
        <td>Turno ${h(c.shift)}</td>
        <td>
          <div class="fw-semibold">${h(c.product_name)}</div>
          <div class="muted tiny mono">${h(c.sku || '')}</div>
          ${legacyBadge}
        </td>
        <td>${h(c.cf_code || '')} ${c.cf_name ? '· ' + h(c.cf_name) : ''}</td>
        <td class="mono text-end">${num(c.qty_cf).toFixed(3)}</td>
        <td class="mono text-end">${num(c.qty_cub).toFixed(3)}</td>
        <td class="mono text-end">${num(c.qty_merma).toFixed(3)}</td>
        <td class="text-end">
          <button class="btn btn-sm btn-outline-info" data-view="${h(c.capture_uid)}">Ver</button>
          <button class="btn btn-sm btn-outline-danger ms-1" data-delcap="${h(c.capture_uid)}" ${locked?'disabled':''}>Borrar</button>
        </td>
      </tr>
    `);
  }
}

function fillModalLists(){
  const barra = products[0] || null;
  const prodOptions = barra ? `<option value="${barra.id}">${h(barra.sku)} · ${h(barra.name)}</option>` : '';
  $('mProd').innerHTML = prodOptions;
  $('eProd').innerHTML = prodOptions;
  if (barra) {
    $('mProd').value = String(barra.id);
    $('eProd').value = String(barra.id);
  }
  // Hielo Barra solo captura Barra 150 kg; se bloquea visualmente para evitar cambios.
  $('mProd').disabled = true;
  $('eProd').disabled = true;

  const cf = warehouses.filter(w => ['CF1','CF2'].includes(String(w.code).toUpperCase()));
  const cfOptions = cf.map(w=>`<option value="${w.id}">${h(w.code)} · ${h(w.name)}</option>`).join('');
  $('mCF').innerHTML = cfOptions;
  $('eCF').innerHTML = cfOptions;
}


async function loadWaterSummary(){
  const date = $('datePick').value;
  const j = await apiGet('water_summary', {date});
  if(!j.ok){ alert(j.error || 'Error al cargar consumo de agua'); return; }
  waterSummary = j;

  const day = j.day || {};
  const meta = j.meta || {};
  $('waterNote').textContent = meta.simulation_note || 'Simulación de consumo de agua.';

  $('waterTopCards').innerHTML = `
    <div class="waterSummaryCard">
      <div class="waterMini">Objetivo del día</div>
      <div class="waterBig mono">${num(day.target_bars).toFixed(0)} barras</div>
      <div class="waterMini mono">${num(day.target_liters).toLocaleString('es-MX',{maximumFractionDigits:0})} L</div>
    </div>
    <div class="waterSummaryCard">
      <div class="waterMini">Reportado del día</div>
      <div class="waterBig mono">${num(day.reported_units).toFixed(3)}</div>
      <div class="waterMini">unidad(es) reportadas</div>
    </div>
    <div class="waterSummaryCard">
      <div class="waterMini">Barras equivalentes</div>
      <div class="waterBig mono">${num(day.equivalent_bars).toFixed(0)}</div>
      <div class="waterMini">1 barra = 150 litros</div>
    </div>
    <div class="waterSummaryCard">
      <div class="waterMini">Agua producida</div>
      <div class="waterBig mono">${num(day.produced_liters).toLocaleString('es-MX',{maximumFractionDigits:0})} L</div>
      <div class="waterMini">CF + CUB</div>
    </div>
    <div class="waterSummaryCard">
      <div class="waterMini">Agua mermada</div>
      <div class="waterBig mono">${num(day.wasted_liters).toLocaleString('es-MX',{maximumFractionDigits:0})} L</div>
      <div class="waterMini">Merma × 150 L</div>
    </div>
    <div class="waterSummaryCard">
      <div class="waterMini">Agua utilizada total</div>
      <div class="waterBig mono">${num(day.consumed_liters).toLocaleString('es-MX',{maximumFractionDigits:0})} L</div>
      <div class="waterMini mono">${Math.round(num(day.progress_pct || 0))}% del objetivo diario</div>
    </div>
  `;

  const shifts = Array.isArray(j.shifts) ? j.shifts : [];
  $('waterTanks').innerHTML = shifts.map(r=>{
    const producedPct = num(r.target_liters) > 0 ? clamp((num(r.produced_liters) / num(r.target_liters)) * 100, 0, 140) : 0;
    const wastedPct = num(r.target_liters) > 0 ? clamp((num(r.wasted_liters) / num(r.target_liters)) * 100, 0, 140) : 0;
    const p = clamp(num(r.progress_pct || 0), 0, 140);
    const wastedBottom = Math.min(producedPct, 140);
    return `
      <div class="waterTank">
        <div class="waterTankHead">
          <div>
            <div class="fw-bold">Turno ${h(r.shift)}</div>
            <div class="waterMini">Objetivo ${num(r.target_bars).toFixed(0)} barras</div>
          </div>
          <span class="pill mono">${Math.round(p)}%</span>
        </div>
        <div class="waterTankBody">
          <div class="waterFillProduced" title="Agua producida" style="height:${producedPct}%"></div>
          <div class="waterFillWasted" title="Agua mermada" style="height:${wastedPct}%; bottom:${wastedBottom}%"></div>
        </div>
        <div class="waterTankMetrics">
          <div class="waterBig mono">${num(r.consumed_liters).toLocaleString('es-MX',{maximumFractionDigits:0})} L</div>
          <div class="waterMini">Agua utilizada del turno</div>
          <div class="waterLegend"><span class="waterLegendItem"><span class="waterDot blue"></span>Producida ${num(r.produced_liters).toLocaleString('es-MX',{maximumFractionDigits:0})} L</span><span class="waterLegendItem"><span class="waterDot red"></span>Mermada ${num(r.wasted_liters).toLocaleString('es-MX',{maximumFractionDigits:0})} L</span></div>
          <div class="waterMetricRow"><span class="waterMini">Reportado</span><b class="mono">${num(r.reported_units).toFixed(3)}</b></div>
          <div class="waterMetricRow"><span class="waterMini">Barras equivalentes</span><b class="mono">${num(r.equivalent_bars).toFixed(0)}</b></div>
          <div class="waterMetricRow"><span class="waterMini">Almacén / CUB / Merma</span><b class="mono">${num(r.qty_cf).toFixed(3)} / ${num(r.qty_cub).toFixed(3)} / ${num(r.qty_merma).toFixed(3)}</b></div>
        </div>
      </div>
    `;
  }).join('');

  $('waterBody').innerHTML = shifts.map(r=>`
    <tr>
      <td><b>Turno ${h(r.shift)}</b></td>
      <td class="text-end mono">${num(r.reported_units).toFixed(3)}</td>
      <td class="text-end mono">${num(r.equivalent_bars).toFixed(0)}</td>
      <td class="text-end mono">${num(r.target_bars).toFixed(0)}</td>
      <td class="text-end mono">${num(r.produced_liters).toLocaleString('es-MX',{maximumFractionDigits:0})}</td>
      <td class="text-end mono">${num(r.wasted_liters).toLocaleString('es-MX',{maximumFractionDigits:0})}</td>
      <td class="text-end mono">${num(r.consumed_liters).toLocaleString('es-MX',{maximumFractionDigits:0})}</td>
      <td class="text-end mono">${num(r.target_liters).toLocaleString('es-MX',{maximumFractionDigits:0})}</td>
      <td class="text-end mono">${num(r.qty_cf).toFixed(3)}</td>
      <td class="text-end mono">${num(r.qty_cub).toFixed(3)}</td>
      <td class="text-end mono">${num(r.qty_merma).toFixed(3)}</td>
    </tr>
  `).join('') + `
    <tr>
      <td><b>Total día</b></td>
      <td class="text-end mono"><b>${num(day.reported_units).toFixed(3)}</b></td>
      <td class="text-end mono"><b>${num(day.equivalent_bars).toFixed(0)}</b></td>
      <td class="text-end mono"><b>${num(day.target_bars).toFixed(0)}</b></td>
      <td class="text-end mono"><b>${num(day.produced_liters).toLocaleString('es-MX',{maximumFractionDigits:0})}</b></td>
      <td class="text-end mono"><b>${num(day.wasted_liters).toLocaleString('es-MX',{maximumFractionDigits:0})}</b></td>
      <td class="text-end mono"><b>${num(day.consumed_liters).toLocaleString('es-MX',{maximumFractionDigits:0})}</b></td>
      <td class="text-end mono"><b>${num(day.target_liters).toLocaleString('es-MX',{maximumFractionDigits:0})}</b></td>
      <td class="text-end mono"><b>${num(day.qty_cf).toFixed(3)}</b></td>
      <td class="text-end mono"><b>${num(day.qty_cub).toFixed(3)}</b></td>
      <td class="text-end mono"><b>${num(day.qty_merma).toFixed(3)}</b></td>
    </tr>`;

  new bootstrap.Modal($('mdlWater')).show();
}

async function loadTimezones(){
  const j = await apiGet('tz_get');
  if (!j.ok) return;
  currentTz = j.timezone || APP_TZ;
  tzOptions = j.options || {};
  $('tzSel').innerHTML = Object.entries(tzOptions).map(([k,v]) => `<option value="${h(k)}">${h(v)} · ${h(k)}</option>`).join('');
  $('tzSel').value = currentTz;
  $('tzBadge').innerHTML = `<i class="bi bi-clock-history"></i> ${h(currentTz)}`;
}


async function loadSignalSettings(){
  const j = await apiGet('signal_settings_get');
  if(!j.ok){ console.warn(j.error || 'No se pudo cargar configuración de señal'); return; }
  signalSettings = j.settings || {};
  signalTags = j.tags || [];

  if ($('sigMode')) $('sigMode').value = signalSettings.report_mode || 'MANUAL';
  if ($('sigEnabled')) $('sigEnabled').value = String(signalSettings.plc_enabled || 0);
  if ($('sigPoll')) $('sigPoll').value = String(signalSettings.poll_seconds || 5);
  if ($('sigQty')) { $('sigQty').value = String(HARVEST_QTY); $('sigQty').readOnly = true; $('sigQty').setAttribute('aria-readonly','true'); }

  const prodRows = j.products || [];
  const whRows = j.warehouses || [];
  if ($('sigProduct')) {
    $('sigProduct').innerHTML = prodRows.map(p=>`<option value="${p.id}">${h(p.sku)} · ${h(p.name)}</option>`).join('');
    $('sigProduct').disabled = true;
    if (prodRows.length) $('sigProduct').value = String(prodRows[0].id);
  }
  if ($('sigCf')) $('sigCf').innerHTML = whRows.map(w=>`<option value="${w.id}">${h(w.code)} · ${h(w.name)}</option>`).join('');
  if ($('sigProduct') && signalSettings.default_product_id && prodRows.some(p => String(p.id) === String(signalSettings.default_product_id))) $('sigProduct').value = String(signalSettings.default_product_id);
  if ($('sigCf') && signalSettings.default_cf_wh_id) $('sigCf').value = String(signalSettings.default_cf_wh_id);

  renderSignalTags();
  renderSignalModeFloating();
  restartSignalPolling();
}

function renderSignalTags(){
  const tb = $('sigTagsBody');
  if (!tb) return;
  if (!signalTags.length){ tb.innerHTML = `<tr><td colspan="6" class="muted">Sin tags configurados.</td></tr>`; return; }
  tb.innerHTML = signalTags.map(t=>`
    <tr>
      <td class="mono">${h(t.tag_code)}</td>
      <td>${h(t.tag_name)}</td>
      <td>${h(t.line_name || '')}</td>
      <td><span class="badge ${Number(t.active)?'text-bg-success':'text-bg-secondary'}">${Number(t.active)?'Activo':'Inactivo'}</span></td>
      <td><span class="badge ${Number(t.signal_state)?'text-bg-warning':'text-bg-dark'} mono">${h(t.signal_state)}</span> <span class="muted small">${h(t.status || '')}</span></td>
      <td class="text-end"><button class="btn btn-sm btn-outline-info" data-edit-sigtag="${h(t.id)}">Editar</button></td>
    </tr>
  `).join('');
}

function renderSignalModeFloating(){
  const btn = $('btnSignalModeFloat');
  const txt = $('signalModeFloatText');
  if (!btn || !txt) return;

  const isAuto = signalSettings
    && String(signalSettings.report_mode || 'MANUAL').toUpperCase() === 'AUTOMATIC'
    && Number(signalSettings.plc_enabled || 0) === 1;

  btn.classList.toggle('is-auto', isAuto);
  btn.classList.toggle('is-manual', !isAuto);
  txt.textContent = isAuto ? 'Automático PLC' : 'Modo Manual';
  btn.title = isAuto
    ? 'Clic para cambiar a modo Manual. Código 2304.'
    : 'Clic para cambiar a Automático PLC. Código 5321.';
}

async function switchSignalModeFromFloating(){
  const currentIsAuto = signalSettings
    && String(signalSettings.report_mode || 'MANUAL').toUpperCase() === 'AUTOMATIC'
    && Number(signalSettings.plc_enabled || 0) === 1;

  const targetMode = currentIsAuto ? 'MANUAL' : 'AUTOMATIC';
  const expected = targetMode === 'AUTOMATIC' ? '5321' : '2304';
  const label = targetMode === 'AUTOMATIC' ? 'Automático PLC' : 'Manual';

  const code = prompt(`Código para cambiar a ${label}:`);
  if (code === null) return;
  if (String(code).trim() !== expected) {
    alert('Código incorrecto. No se cambió el modo.');
    return;
  }

  const btn = $('btnSignalModeFloat');
  if (btn) btn.disabled = true;

  const j = await apiPost('signal_mode_switch', {
    target_mode: targetMode,
    auth_code: String(code).trim()
  });

  if (btn) btn.disabled = false;

  if (!j.ok) {
    alert(j.error || 'No se pudo cambiar el modo.');
    return;
  }

  signalSettings = j.settings || signalSettings || {};
  renderSignalModeFloating();
  restartSignalPolling();
  await loadSignalSettings();
  alert(j.message || 'Modo actualizado.');
}

async function saveSignalSettings(){
  const j = await apiPost('signal_settings_save', {
    report_mode: $('sigMode').value,
    plc_enabled: $('sigEnabled').value === '1' ? 1 : 0,
    poll_seconds: parseInt($('sigPoll').value || '5', 10),
    default_product_id: parseInt($('sigProduct').value || '0', 10),
    default_cf_wh_id: parseInt($('sigCf').value || '0', 10),
    default_total_qty: HARVEST_QTY
  });
  if(!j.ok){ alert(j.error || 'Error guardando configuración'); return; }
  await loadSignalSettings();
  alert('Configuración de señal actualizada.');
}

async function saveSignalTag(){
  const payload = {
    id: parseInt($('sigTagId').value || '0', 10),
    tag_code: $('sigTagCode').value,
    tag_name: $('sigTagName').value,
    line_name: $('sigLineName').value,
    active: $('sigTagActive').value === '1' ? 1 : 0
  };
  const j = await apiPost('signal_tag_save', payload);
  if(!j.ok){ alert(j.error || 'Error guardando tag'); return; }
  $('sigTagId').value = '0'; $('sigTagCode').value = ''; $('sigTagName').value = ''; $('sigLineName').value = ''; $('sigTagActive').value = '1';
  await loadSignalSettings();
}

function restartSignalPolling(){
  if (signalPollTimer){ clearInterval(signalPollTimer); signalPollTimer = null; }
  if (!signalSettings || signalSettings.report_mode !== 'AUTOMATIC' || Number(signalSettings.plc_enabled) !== 1) return;
  const seconds = Math.max(3, Math.min(60, parseInt(signalSettings.poll_seconds || '5', 10)));
  signalPollTimer = setInterval(checkPendingSignal, seconds * 1000);
  setTimeout(checkPendingSignal, 800);
}

async function checkPendingSignal(){
  if (activeSignalTag) return;
  if (!running) return;
  const j = await apiGet('signal_pending');
  if(!j.ok){ console.warn(j.error || 'Error leyendo señal'); return; }
  if (!j.pending) return;
  activeSignalTag = j.tag;
  showSignalDecisionModal(activeSignalTag, j.settings || signalSettings || {});
}

function showSignalDecisionModal(tag, settings){
  if ($('plcSignalPanel')) $('plcSignalPanel').style.display = 'block';
  if ($('plcSignalText')) $('plcSignalText').textContent = `${tag.tag_name || tag.tag_code} · ${tag.line_name || ''}`;
  if ($('plcSignalBadge')) $('plcSignalBadge').textContent = String(tag.signal_state || 1);
  $('sigModalTag').textContent = tag.tag_code || '';
  $('sigModalLine').textContent = tag.line_name || tag.tag_name || '';
  $('sigModalTime').textContent = String(tag.last_value_at || tag.opened_at || '').replace('T',' ');
  if ($('sigMaintNotes')) $('sigMaintNotes').value = '';
  new bootstrap.Modal($('mdlSignal')).show();
}

function openProductionFromSignal(){
  if (!activeSignalTag) return;
  bootstrap.Modal.getInstance($('mdlSignal'))?.hide();
  setNowLocal('mTime');
  resetBundleDefaults();
  if (signalSettings || activeSignalTag) {
    // Prioridad: configuración del tag PLC; si viene vacía, usar configuración general.
    const prodId = products[0]?.id || signalSettings?.default_product_id || 0; // Hielo Barra fuerza Barra 150 kg
    const cfId   = activeSignalTag?.cf_wh_id || signalSettings?.default_cf_wh_id || 0;
    if (prodId && $('mProd')) $('mProd').value = String(prodId);
    if (cfId && $('mCF')) $('mCF').value = String(cfId);
    if ($('mTotal')) $('mTotal').value = '10';
  }
  recomputeAddCF();
  new bootstrap.Modal($('mdl')).show();
}

async function registerMaintenanceFromSignal(){
  if (!activeSignalTag) return;
  const j = await apiPost('signal_maintenance', {
    tag_id: parseInt(activeSignalTag.id, 10),
    date: $('datePick').value,
    notes: $('sigMaintNotes').value
  });
  if(!j.ok){ alert(j.error || 'Error registrando mantenimiento'); return; }
  bootstrap.Modal.getInstance($('mdlSignal'))?.hide();
  if ($('plcSignalPanel')) $('plcSignalPanel').style.display = 'none';
  activeSignalTag = null;
  await loadSignalSettings();
  await loadAll();
}

async function loadAll(){
  const date = $('datePick').value;

  const pj = await apiGet('products');
  if(!pj.ok){ alert(pj.error || 'Error productos'); return; }
  products = pj.rows || [];

  const wj = await apiGet('warehouses');
  if(!wj.ok){ alert(wj.error || 'Error almacenes'); return; }
  warehouses = wj.rows || [];

  let shiftToLoad = 0;
  if (running && running.shift) {
    shiftToLoad = parseInt(running.shift, 10) || 0;
  }

  const dj = await apiGet('day_load', {date, shift: shiftToLoad});
  if(!dj.ok){ alert(dj.error || 'Error día'); return; }

  day = dj.day || {};
  byShift = dj.by_shift || {1:0,2:0,3:0};
  byWh = dj.by_wh || [];
  captures = dj.captures || [];
  running = dj.running || null;
  mermaByShift = dj.merma_by_shift || {1:0,2:0,3:0};
  mermaCurrent = num(dj.merma_current || 0);
  displayShift = parseInt(dj.display_shift || 1, 10) || 1;
  selectedShift = parseInt(dj.selected_shift || displayShift || 1, 10) || 1;

  $('antennaVal').value = num(day.antenna_total_qty).toFixed(3);

  renderTargets();
  renderHeaderTotals();
  renderKpis();
  renderWh();
  renderLog();
  fillModalLists();
  applyShiftUI();
}

function resetBundleDefaults(){
  $('mTotal').value = String(HARVEST_QTY);
  $('qCUB').value = '0';
  $('qMER').value = '0';
  $('qCF').value = String(HARVEST_QTY);
  $('mTotalLbl').textContent = String(HARVEST_QTY);
  $('sumLbl').textContent = String(HARVEST_QTY);
  $('diffLbl').textContent = '0';
}

function recomputeAddCF(){
  let total = HARVEST_QTY;
  let cub = Math.max(0, Math.round(num($('qCUB').value)));
  let mer = Math.max(0, Math.round(num($('qMER').value)));

  if (cub + mer > total){
    let over = (cub + mer) - total;
    const r1 = Math.min(mer, over); mer -= r1; over -= r1;
    const r2 = Math.min(cub, over); cub -= r2;
  }

  const cf = total - (cub + mer);

  $('mTotal').value = String(total);
  $('qCUB').value = String(cub);
  $('qMER').value = String(mer);
  $('qCF').value = String(cf);

  const sum = cf + cub + mer;
  $('mTotalLbl').textContent = String(total);
  $('sumLbl').textContent = String(sum);
  $('diffLbl').textContent = String(total - sum);
}

function recomputeEditCF(){
  let total = Math.max(1, Math.round(num($('eTotal').value)));
  let cub = Math.max(0, Math.round(num($('eQCUB').value)));
  let mer = Math.max(0, Math.round(num($('eQMER').value)));

  if (cub + mer > total){
    let over = (cub + mer) - total;
    const r1 = Math.min(mer, over); mer -= r1; over -= r1;
    const r2 = Math.min(cub, over); cub -= r2;
  }

  const cf = total - (cub + mer);

  $('eTotal').value = String(total);
  $('eQCUB').value = String(cub);
  $('eQMER').value = String(mer);
  $('eQCF').value = String(cf);

  const sum = cf + cub + mer;
  $('eSumLbl').textContent = String(sum);
  $('eDiffLbl').textContent = String(total - sum);
}

function buildBundlePayload(){
  return {
    date: $('datePick').value,
    product_id: parseInt($('mProd').value,10),
    entry_time: $('mTime').value,
    total: HARVEST_QTY,
    cf_wh_id: parseInt($('mCF').value,10),
    q_cf: Math.round(num($('qCF').value)),
    q_cub: Math.round(num($('qCUB').value)),
    q_merma: Math.round(num($('qMER').value)),
    plc_tag_id: activeSignalTag ? parseInt(activeSignalTag.id, 10) : 0,
  };
}
function buildEditPayload(){
  return {
    capture_uid: $('eCaptureUid').value,
    date: $('eDate').value,
    product_id: parseInt($('eProd').value,10),
    entry_time: $('eTime').value,
    total: Math.round(num($('eTotal').value)),
    cf_wh_id: parseInt($('eCF').value,10),
    q_cf: Math.round(num($('eQCF').value)),
    q_cub: Math.round(num($('eQCUB').value)),
    q_merma: Math.round(num($('eQMER').value)),
  };
}

function openConfirm(payload){
  const sum = payload.q_cf + payload.q_cub + payload.q_merma;
  const cfText = $('mCF').selectedOptions?.[0]?.textContent || 'CF';

  $('confirmBody').innerHTML = `
    <div class="alert alert-warning">Confirma el registro de producción.</div>
    <div class="cardInner p-3">
      <div class="d-flex justify-content-between"><span>Total cosecha</span><b class="mono">${payload.total}</b></div>
      <div class="d-flex justify-content-between"><span>${h(cfText)}</span><b class="mono">${payload.q_cf}</b></div>
      <div class="d-flex justify-content-between"><span>CUB</span><b class="mono">${payload.q_cub}</b></div>
      <div class="d-flex justify-content-between"><span>Merma</span><b class="mono">${payload.q_merma}</b></div>
      <hr class="border-light opacity-25 my-2">
      <div class="d-flex justify-content-between"><span>Suma</span><b class="mono">${sum}</b></div>
      <div class="muted small mt-2">Hora: <span class="mono">${h(String(payload.entry_time || '').replace('T',' '))}</span></div>
    </div>
  `;
  pendingBundle = payload;
  new bootstrap.Modal($('mdlConfirm')).show();
}


function printDirectUrl(url){
  const printUrl = (url instanceof URL) ? url.toString() : String(url);
  const win = window.open(printUrl, '_blank', 'noopener,noreferrer');
  if (!win) alert('El navegador bloqueó la ventana de impresión. Permite ventanas emergentes para este sitio.');
}

function renderShiftSummaryHtml(s, date, shift){
  const t = s.summary || {};
  const rows = s.conversions || [];
  const runningMsg = s.is_running
    ? `<div class="alert alert-warning"><i class="bi bi-hourglass-split"></i> <b>Turno ${h(shift)} en curso.</b> El reporte puede cambiar hasta que se finalice el turno.</div>`
    : `<div class="alert alert-info"><i class="bi bi-check-circle"></i> Resumen histórico del <b>Turno ${h(shift)}</b> para la fecha <span class="mono">${h(date)}</span>.</div>`;

  const convRows = rows.length
    ? rows.map(r => `
        <tr>
          <td>${h(r.sku || '')} · ${h(r.product_name || '')}</td>
          <td class="text-end mono">${num(r.factor).toFixed(3)}</td>
          <td class="text-end mono">${num(r.output_qty).toFixed(3)}</td>
          <td class="text-end mono">${num(r.bars_taken).toFixed(3)}</td>
          <td class="text-end mono">${num(r.bars_used).toFixed(3)}</td>
          <td class="text-end mono">${num(r.bars_leftover).toFixed(3)}</td>
        </tr>
      `).join('')
    : `<tr><td colspan="6" class="muted text-center">Sin conversiones registradas en este turno.</td></tr>`;

  return `
    ${runningMsg}
    <div class="summaryGrid mb-3">
      <div class="summaryTile"><div class="label">Barras directas a CF</div><div class="value mono">${num(t.bars_cf).toFixed(3)}</div></div>
      <div class="summaryTile"><div class="label">Barras enviadas a CUB</div><div class="value mono">${num(t.bars_cub).toFixed(3)}</div></div>
      <div class="summaryTile"><div class="label">Merma del turno</div><div class="value mono">${num(t.bars_merma).toFixed(3)}</div></div>
      <div class="summaryTile"><div class="label">Total barras del turno</div><div class="value mono">${num(t.bars_total_turno).toFixed(3)}</div></div>
    </div>
    <div class="tableWrap">
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead>
            <tr>
              <th>Artículo convertido</th>
              <th class="text-end mono">Factor</th>
              <th class="text-end mono">Piezas producidas</th>
              <th class="text-end mono">Barras tomadas</th>
              <th class="text-end mono">Barras convertidas</th>
              <th class="text-end mono">Sobrante</th>
            </tr>
          </thead>
          <tbody>${convRows}</tbody>
        </table>
      </div>
    </div>
    <div class="muted small mt-2">
      El reporte imprimible incluye líneas de firma para <b>Líder</b>, <b>Supervisor</b> y <b>Calidad</b>.
    </div>
  `;
}

async function openShiftSummaryModal(shift){
  const date = $('datePick').value;
  shift = parseInt(shift, 10);
  if (![1,2,3].includes(shift)) return;

  $('shiftSummaryBody').innerHTML = `<div class="alert alert-info mb-0">Cargando resumen del turno ${h(shift)}...</div>`;
  $('shiftSummaryFooter').innerHTML = `<button class="btn btn-outline-light" data-bs-dismiss="modal">Cerrar</button>`;
  const mdl = bootstrap.Modal.getOrCreateInstance($('mdlShiftSummary'));
  mdl.show();

  const s = await apiPost('shift_summary', {date, shift});
  if(!s.ok){
    $('shiftSummaryBody').innerHTML = `<div class="alert alert-danger mb-0">${h(s.error || 'Error al cargar resumen')}</div>`;
    return;
  }

  $('shiftSummaryBody').innerHTML = renderShiftSummaryHtml(s, date, shift);

  const pdfUrl = new URL(location.href);
  pdfUrl.search = '';
  pdfUrl.searchParams.set('action','shift_summary_pdf');
  pdfUrl.searchParams.set('date', date);
  pdfUrl.searchParams.set('shift', String(shift));

  const printUrl = new URL(location.href);
  printUrl.search = '';
  printUrl.searchParams.set('action','shift_print_report');
  printUrl.searchParams.set('date', date);
  printUrl.searchParams.set('shift', String(shift));
  printUrl.searchParams.set('auto_print', '1');

  $('shiftSummaryFooter').innerHTML = `
    <button class="btn btn-outline-light" data-bs-dismiss="modal">Cerrar</button>
    <button type="button" class="btn btn-outline-primary" onclick="printDirectUrl('${printUrl.toString()}')"><i class="bi bi-printer"></i> Imprimir directo</button>
  `;
}


async function openDaySummaryModal(){
  const date = $('datePick').value;
  $('shiftSummaryBody').innerHTML = `<div class="alert alert-info mb-0">Preparando concentrado del día <span class="mono">${h(date)}</span>...</div>`;
  $('shiftSummaryFooter').innerHTML = `<button class="btn btn-outline-light" data-bs-dismiss="modal">Cerrar</button>`;
  const mdl = bootstrap.Modal.getOrCreateInstance($('mdlShiftSummary'));
  mdl.show();

  const summaries = [];
  for (const shift of [1,2,3]) {
    const s = await apiPost('shift_summary', {date, shift});
    if (s.ok) summaries.push(s);
  }

  const totals = summaries.reduce((acc, s) => {
    const t = s.summary || {};
    acc.cf += num(t.bars_cf);
    acc.cub += num(t.bars_cub);
    acc.merma += num(t.bars_merma);
    acc.total += num(t.bars_total_turno);
    acc.convOutput += num(t.conv_output_qty);
    acc.convTaken += num(t.conv_bars_taken);
    acc.convUsed += num(t.conv_bars_used);
    acc.leftover += num(t.conv_bars_leftover);
    return acc;
  }, {cf:0,cub:0,merma:0,total:0,convOutput:0,convTaken:0,convUsed:0,leftover:0});

  const rows = summaries.map(s => {
    const t = s.summary || {};
    return `
      <tr>
        <td class="fw-semibold">Turno ${h(s.shift)}</td>
        <td class="text-end mono">${num(t.bars_cf).toFixed(3)}</td>
        <td class="text-end mono">${num(t.bars_cub).toFixed(3)}</td>
        <td class="text-end mono">${num(t.bars_merma).toFixed(3)}</td>
        <td class="text-end mono fw-bold">${num(t.bars_total_turno).toFixed(3)}</td>
      </tr>
    `;
  }).join('');

  $('shiftSummaryBody').innerHTML = `
    <div class="alert alert-info"><i class="bi bi-calendar-check"></i> <b>Concentrado diario</b> · Fecha <span class="mono">${h(date)}</span></div>
    <div class="summaryGrid mb-3">
      <div class="summaryTile"><div class="label">Barras directas a CF</div><div class="value mono">${totals.cf.toFixed(3)}</div></div>
      <div class="summaryTile"><div class="label">Barras enviadas a CUB</div><div class="value mono">${totals.cub.toFixed(3)}</div></div>
      <div class="summaryTile"><div class="label">Merma de producción</div><div class="value mono">${totals.merma.toFixed(3)}</div></div>
      <div class="summaryTile"><div class="label">Total del día</div><div class="value mono">${totals.total.toFixed(3)}</div></div>
    </div>
    <div class="tableWrap mb-3">
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead><tr><th>Turno</th><th class="text-end">CF</th><th class="text-end">CUB</th><th class="text-end">Merma prod.</th><th class="text-end">Total</th></tr></thead>
          <tbody>${rows || `<tr><td colspan="5" class="text-center muted">Sin datos para la fecha seleccionada.</td></tr>`}</tbody>
        </table>
      </div>
    </div>
    <div class="cardInner p-3">
      <div class="d-flex justify-content-between"><span>Piezas convertidas</span><b class="mono">${totals.convOutput.toFixed(3)}</b></div>
      <div class="d-flex justify-content-between"><span>Barras tomadas conversión</span><b class="mono">${totals.convTaken.toFixed(3)}</b></div>
      <div class="d-flex justify-content-between"><span>Barras convertidas</span><b class="mono">${totals.convUsed.toFixed(3)}</b></div>
      <div class="d-flex justify-content-between"><span>Sobrante</span><b class="mono">${totals.leftover.toFixed(3)}</b></div>
    </div>
    <div class="muted small mt-2">Este concentrado excluye merma registrada desde almacén; solo considera merma de producción.</div>
  `;

  const pdfUrl = new URL(location.href);
  pdfUrl.search = '';
  pdfUrl.searchParams.set('action','day_summary_pdf');
  pdfUrl.searchParams.set('date', date);

  const printUrl = new URL(location.href);
  printUrl.search = '';
  printUrl.searchParams.set('action','day_print_report');
  printUrl.searchParams.set('date', date);
  printUrl.searchParams.set('auto_print', '1');

  $('shiftSummaryFooter').innerHTML = `
    <button class="btn btn-outline-light" data-bs-dismiss="modal">Cerrar</button>
    <button type="button" class="btn btn-outline-primary" onclick="printDirectUrl('${printUrl.toString()}')"><i class="bi bi-printer"></i> Imprimir directo</button>
  `;
}

async function doShiftStart(){
  const j = await apiPost('shift_start', {date: $('datePick').value, shift: parseInt($('shiftSel').value,10)});
  if(!j.ok){ alert(j.error || 'Error'); return; }
  bootstrap.Modal.getInstance($('mdlShift')).hide();
  await loadAll();
}

async function openEndModal(){
  if (!running){ alert('No hay turno activo.'); return; }

  $('endBody').innerHTML = `
    <div class="alert alert-warning mb-0">
      ¿Deseas <b>finalizar el turno</b>?<br>
      Al confirmar se mostrará un resumen y podrás descargar PDF.
    </div>
  `;
  $('endFooter').innerHTML = `
    <button class="btn btn-outline-light" data-bs-dismiss="modal">Cancelar</button>
    <button class="btn btn-accent" id="endGo"><i class="bi bi-arrow-right-circle"></i> Confirmar</button>
  `;

  new bootstrap.Modal($('mdlEnd')).show();
  setTimeout(()=>{ const b = $('endGo'); if (b) b.onclick = loadEndSummary; }, 40);
}

async function loadEndSummary(){
  const date = $('datePick').value;
  const s = await apiPost('shift_summary', {date, shift: running ? parseInt(running.shift, 10) : 0});
  if(!s.ok){ alert(s.error || 'Error'); return; }

  const t = s.summary || {};
  const rows = s.conversions || [];

  const convRows = rows.length
    ? rows.map(r => `
        <tr>
          <td>${h(r.sku || '')} · ${h(r.product_name || '')}</td>
          <td class="text-end mono">${num(r.factor).toFixed(3)}</td>
          <td class="text-end mono">${num(r.output_qty).toFixed(3)}</td>
          <td class="text-end mono">${num(r.bars_taken).toFixed(3)}</td>
          <td class="text-end mono">${num(r.bars_used).toFixed(3)}</td>
          <td class="text-end mono">${num(r.bars_leftover).toFixed(3)}</td>
        </tr>
      `).join('')
    : `<tr><td colspan="6" class="muted text-center">Sin conversiones registradas en este turno.</td></tr>`;

  $('endBody').innerHTML = `
    <div class="alert alert-info">
      <b>Resumen Turno ${h(s.shift)}</b> · Fecha <span class="mono">${h(s.meta?.date || date)}</span>
    </div>

    <div class="cardInner p-3 mb-3">
      <div class="d-flex justify-content-between"><span>Barras directas a CF</span><b class="mono">${num(t.bars_cf).toFixed(3)}</b></div>
      <div class="d-flex justify-content-between"><span>Barras enviadas a CUB</span><b class="mono">${num(t.bars_cub).toFixed(3)}</b></div>
      <div class="d-flex justify-content-between"><span>Merma del turno</span><b class="mono">${num(t.bars_merma).toFixed(3)}</b></div>
      <hr class="border-light opacity-25 my-2">
      <div class="d-flex justify-content-between"><span><b>Total barras del turno</b></span><b class="mono">${num(t.bars_total_turno).toFixed(3)}</b></div>
    </div>

    <div class="tableWrap">
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead>
            <tr>
              <th>Artículo</th>
              <th class="text-end mono">Factor</th>
              <th class="text-end mono">Pzas</th>
              <th class="text-end mono">Tomadas</th>
              <th class="text-end mono">Convertidas</th>
              <th class="text-end mono">Sobrante</th>
            </tr>
          </thead>
          <tbody>
            ${convRows}
          </tbody>
        </table>
      </div>
    </div>

    <div class="muted small mt-2">
      El formato imprimible incluirá líneas de firma para <b>Líder</b>, <b>Supervisor</b> y <b>Calidad</b>.
    </div>
  `;

  const pdfUrl = new URL(location.href);
  pdfUrl.search = '';
  pdfUrl.searchParams.set('action','shift_summary_pdf');
  pdfUrl.searchParams.set('date', date);
  if (s.shift) pdfUrl.searchParams.set('shift', String(s.shift));

  const printUrl = new URL(location.href);
  printUrl.search = '';
  printUrl.searchParams.set('action','shift_print_report');
  printUrl.searchParams.set('date', date);
  if (s.shift) printUrl.searchParams.set('shift', String(s.shift));

  $('endFooter').innerHTML = `
    <button class="btn btn-outline-light" data-bs-dismiss="modal">Cerrar</button>
    
    <a class="btn btn-outline-primary" href="${printUrl.toString()}" target="_blank">
      <i class="bi bi-printer"></i> Imprimir reporte
    </a>
    <button class="btn btn-outline-danger" id="endYes">
      <i class="bi bi-stop-fill"></i> Finalizar turno
    </button>
  `;

  setTimeout(()=>{ const b = $('endYes'); if (b) b.onclick = doEndYes; }, 40);
}

async function doEndYes(){
  const j = await apiPost('shift_end', {date: $('datePick').value});
  if(!j.ok){ alert(j.error || 'Error'); return; }
  bootstrap.Modal.getInstance($('mdlEnd')).hide();
  await loadAll();
}

async function saveTargets(){
  const j = await apiPost('targets_save', {
    date: $('datePick').value,
    target_t1: num($('t1').value),
    target_t2: num($('t2').value),
    target_t3: num($('t3').value),
    target_total: num($('tt').value)
  });
  if(!j.ok){ alert(j.error || 'Error'); return; }
  await loadAll();
}

async function saveAntenna(){
  const j = await apiPost('antenna_save', {
    date: $('datePick').value,
    antenna_total_qty: num($('antennaVal').value)
  });
  if(!j.ok){ alert(j.error || 'Error'); return; }
  await loadAll();
}

async function toggleLock(){
  const lock = num(day?.locked || 0) ? 0 : 1;
  const j = await apiPost('day_lock', {date: $('datePick').value, lock});
  if(!j.ok){ alert(j.error || 'Error'); return; }
  await loadAll();
}

async function saveTimezone(){
  const j = await apiPost('tz_save', {timezone: $('tzSel').value});
  if(!j.ok){ alert(j.error || 'Error'); return; }
  await loadTimezones();
  await loadSignalSettings();
  await loadAll();
  alert('Zona horaria actualizada.');
}

async function confirmYes(){
  if (!pendingBundle) return;
  const j = await apiPost('entry_add_bundle', pendingBundle);
  if(!j.ok){ alert(j.error || 'Error al guardar'); return; }
  bootstrap.Modal.getInstance($('mdlConfirm')).hide();
  bootstrap.Modal.getInstance($('mdl')).hide();
  pendingBundle = null;
  if (activeSignalTag) {
    activeSignalTag = null;
    if ($('plcSignalPanel')) $('plcSignalPanel').style.display = 'none';
    await loadSignalSettings();
  }
  await loadAll();
}

async function openCapture(uid){
  const j = await apiGet('capture_get', {capture_uid: uid});
  if(!j.ok){ alert(j.error || 'Error'); return; }

  const r = j.row;

  $('viewCaptureBody').innerHTML = `
    <div class="captureSummary">
      <div class="captureItem"><div class="captureLabel">Hora</div><div class="captureValue mono">${h(String(r.entry_time || '').replace('T',' '))}</div></div>
      <div class="captureItem"><div class="captureLabel">Turno</div><div class="captureValue">T${h(r.shift)}</div></div>
      <div class="captureItem"><div class="captureLabel">Producto</div><div class="captureValue smallx">${h(r.product_name)}</div></div>
      <div class="captureItem"><div class="captureLabel">Captura UID</div><div class="captureValue mono smallx">${h(r.capture_uid)}</div></div>
      <div class="captureItem"><div class="captureLabel">Destino CF</div><div class="captureValue">${h(r.cf_code || '')} ${r.cf_name ? '· '+h(r.cf_name) : ''}</div></div>
      <div class="captureItem"><div class="captureLabel">Total</div><div class="captureValue mono">${num(r.qty_total).toFixed(3)}</div></div>
      <div class="captureItem"><div class="captureLabel">CUB</div><div class="captureValue mono">${num(r.qty_cub).toFixed(3)}</div></div>
      <div class="captureItem"><div class="captureLabel">Merma</div><div class="captureValue mono">${num(r.qty_merma).toFixed(3)}</div></div>
    </div>
  `;

  $('eCaptureUid').value = r.capture_uid;
  $('eDate').value = String(r.work_date || '');
  $('eTime').value = sqlDateTimeToLocalInput(r.entry_time);
  $('eProd').value = String(r.product_id);
  $('eTotal').value = String(Math.round(num(r.qty_total)));
  $('eCF').value = String(r.cf_wh_id || '');
  $('eQCUB').value = String(Math.round(num(r.qty_cub)));
  $('eQMER').value = String(Math.round(num(r.qty_merma)));
  recomputeEditCF();

  new bootstrap.Modal($('mdlViewCapture')).show();
}

async function updateCapture(){
  const payload = buildEditPayload();
  const sum = payload.q_cf + payload.q_cub + payload.q_merma;

  if (!payload.capture_uid){ alert('Captura inválida'); return; }
  if (!payload.product_id){ alert('Producto inválido'); return; }
  if (!payload.cf_wh_id){ alert('Selecciona destino CF'); return; }
  if (payload.total <= 0){ alert('Total inválido'); return; }
  if (sum !== payload.total){
    alert(`La suma debe coincidir con el total. Total=${payload.total}, Suma=${sum}`);
    return;
  }

  const j = await apiPost('capture_update', payload);
  if(!j.ok){ alert(j.error || 'Error al actualizar'); return; }

  bootstrap.Modal.getInstance($('mdlViewCapture')).hide();
  await loadAll();
}

async function deleteCapture(uid){
  const j = await apiPost('capture_delete', {capture_uid: uid});
  if(!j.ok){ alert(j.error || 'Error'); return; }
  await loadAll();
}

document.addEventListener('DOMContentLoaded', async ()=>{
  await loadTimezones();
  await loadAll();
  await loadSignalSettings();

  $('datePick').addEventListener('change', loadAll);

  document.querySelectorAll('[data-shift-card]').forEach(card => {
    card.addEventListener('click', () => openShiftSummaryModal(card.getAttribute('data-shift-card')));
    card.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        openShiftSummaryModal(card.getAttribute('data-shift-card'));
      }
    });
  });

  document.querySelectorAll('[data-day-total-card]').forEach(card => {
    card.addEventListener('click', () => openDaySummaryModal());
    card.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        openDaySummaryModal();
      }
    });
  });

  $('btnStartShift').addEventListener('click', ()=> new bootstrap.Modal($('mdlShift')).show());
  $('doShiftStart').addEventListener('click', doShiftStart);

  $('btnEndShift').addEventListener('click', openEndModal);

  $('btnSettings').addEventListener('click', ()=> new bootstrap.Modal($('mdlSettings')).show());
  $('btnSignalModeFloat')?.addEventListener('click', switchSignalModeFromFloating);
  $('btnWater').addEventListener('click', loadWaterSummary);
  $('btnSaveTargets').addEventListener('click', saveTargets);
  $('saveAntenna').addEventListener('click', saveAntenna);
  $('btnSaveTz').addEventListener('click', saveTimezone);
  $('btnSaveSignalSettings')?.addEventListener('click', saveSignalSettings);
  $('btnSaveSignalTag')?.addEventListener('click', saveSignalTag);
  $('sigTagsBody')?.addEventListener('click', (e)=>{
    const btn = e.target.closest('[data-edit-sigtag]');
    if (!btn) return;
    const id = parseInt(btn.getAttribute('data-edit-sigtag'), 10);
    const t = signalTags.find(x => parseInt(x.id,10) === id);
    if (!t) return;
    $('sigTagId').value = String(t.id);
    $('sigTagCode').value = t.tag_code || '';
    $('sigTagName').value = t.tag_name || '';
    $('sigLineName').value = t.line_name || '';
    $('sigTagActive').value = String(Number(t.active) ? 1 : 0);
  });
  $('btnSignalProduction')?.addEventListener('click', openProductionFromSignal);
  $('btnSignalMaintenance')?.addEventListener('click', registerMaintenanceFromSignal);

  const lockBtn = $('btnLock');
  if (lockBtn) lockBtn.addEventListener('click', toggleLock);

  $('btnAdd').addEventListener('click', ()=>{
    activeSignalTag = null;
    if (!running){
      alert('Debes iniciar turno antes de reportar producción.');
      return;
    }
    setNowLocal('mTime');
    resetBundleDefaults();
    recomputeAddCF();
    new bootstrap.Modal($('mdl')).show();
  });

  ['qCUB','qMER'].forEach(id=> $(id).addEventListener('input', recomputeAddCF));
  ['eTotal','eQCUB','eQMER'].forEach(id=> $(id).addEventListener('input', recomputeEditCF));

  $('mSave').addEventListener('click', ()=>{
    recomputeAddCF();
    const payload = buildBundlePayload();
    const sum = payload.q_cf + payload.q_cub + payload.q_merma;

    if (payload.total <= 0){ alert('Total inválido'); return; }
    if (!payload.cf_wh_id){ alert('Selecciona destino CF'); return; }
    if (sum !== payload.total){
      alert(`La suma debe ser igual al total. Total=${payload.total}, Suma=${sum}`);
      return;
    }
    openConfirm(payload);
  });

  $('confirmYes').addEventListener('click', confirmYes);
  $('btnUpdateCapture').addEventListener('click', updateCapture);

  $('btnDeleteCapture').addEventListener('click', async ()=>{
    const uid = $('eCaptureUid').value;
    if (!uid) return;
    if (!confirm('¿Borrar la captura completa?')) return;
    bootstrap.Modal.getInstance($('mdlViewCapture')).hide();
    await deleteCapture(uid);
  });

  $('logBody').addEventListener('click', async (e)=>{
    const bView = e.target.closest('button[data-view]');
    if (bView){
      const uid = bView.getAttribute('data-view');
      if (uid) await openCapture(uid);
      return;
    }

    const bDel = e.target.closest('button[data-delcap]');
    if (bDel){
      const uid = bDel.getAttribute('data-delcap');
      if (!uid) return;
      if (!confirm('¿Borrar esta captura completa?')) return;
      await deleteCapture(uid);
    }
  });

  setInterval(async ()=>{
    await apiPost('shift_heartbeat', {date: $('datePick').value});
    const s = await apiGet('shift_status', {date: $('datePick').value});
    if (s.ok){
      running = s.running || null;
      applyShiftUI();
    }
  }, 20000);
});
</script>
</body>
</html>